{
  "openapi": "3.0.3",
  "info": {
    "title": "MangoApps API",
    "description": "# MangoApps API Documentation\n\nThis API provides access to the MangoApps platform, allowing you to:\n- Manage shifts and schedules\n- Get notified of changes via webhooks\n- Upload and retrieve demand forecasts\n- Perform bulk operations on shifts, assignments, and more\n- Mobile authentication and user management\n\n## Authentication\n\nThe API supports multiple authentication methods:\n\n### Mobile App Authentication\n```\nPOST /api/v1/auth/login\n{\n  \"email\": \"user@example.com\",\n  \"password\": \"password\",\n  \"otp_code\": \"123456\" // Optional, required if 2FA is enabled\n}\n```\n\nResponse includes `access_token` and `refresh_token` for subsequent requests.\n\n### API Token Authentication\nInclude the token in the Authorization header:\n```\nAuthorization: Bearer YOUR_API_TOKEN\n```\n\n### Service Account Authentication\nFor service accounts, include both token and secret:\n```\nAuthorization: Bearer YOUR_TOKEN\nX-API-Secret: YOUR_SECRET\n```\n\nService accounts with the `admin` scope have full access to all API endpoints and bypass\nindividual scope checks. Other service accounts require specific scopes (e.g., `read:shifts`,\n`write:attendance`) for each endpoint.\n\n### Impersonation Token Authentication\nService accounts with `impersonate` or `admin` scope can create short-lived impersonation\ntokens to act on behalf of specific users. This is useful for accessing user-specific data\n(e.g., leave balances, personal preferences) without creating separate admin endpoints.\n\nTo use impersonation:\n1. Create an impersonation token using your service account:\n   ```\n   POST /api/v1/auth/impersonate\n   Authorization: Bearer YOUR_SERVICE_ACCOUNT_TOKEN\n   X-API-Secret: YOUR_SERVICE_ACCOUNT_SECRET\n   {\n     \"user_id\": 123\n   }\n   ```\n2. Use the returned impersonation token for subsequent API calls:\n   ```\n   Authorization: Bearer IMPERSONATION_TOKEN\n   ```\n\nImpersonation tokens:\n- Are short-lived (default: 1 hour)\n- Can be revoked at any time\n- Automatically log all access for audit purposes\n- Allow access to user-specific endpoints as if authenticated as that user\n\n## Rate Limiting\nAPI requests are limited to 100 requests per minute per API token.\nRate limit headers are included in responses:\n- `X-RateLimit-Limit`: Request limit per window\n- `X-RateLimit-Remaining`: Requests remaining in current window  \n- `X-RateLimit-Reset`: Unix timestamp when window resets\n\n## Pagination\nCollections return pagination metadata and headers:\n- Query parameters: `page`, `per_page`\n- Response headers: `X-Total-Count`, `X-Total-Pages`, `X-Current-Page`, `X-Per-Page`\n\n## Error Handling\nErrors follow a consistent format:\n```json\n{\n  \"error\": {\n    \"code\": \"error_code\",\n    \"message\": \"Human readable message\",\n    \"details\": {} // Optional additional context\n  }\n}\n```\n\nValidation errors return:\n```json\n{\n  \"errors\": [\n    {\n      \"field\": \"email\",\n      \"message\": \"Email is required\",\n      \"code\": \"validation_error\"\n    }\n  ]\n}\n```\n\n## Piggyback Response System\n\nThe API supports a piggyback response system that allows including additional contextual data \nalongside the primary response. This reduces network round-trips by proactively sending data \nthe client might need next.\n\n### Requesting Piggyback Data\n\nUse the `include` query parameter to request specific piggyback data:\n```\nGET /api/v1/users/me?include=business_settings,user_preferences,upcoming_shifts\n```\n\nAvailable piggyback data types:\n- `business_settings` - Business configuration and branding\n- `user_preferences` - User settings and preferences  \n- `recent_shifts` - Last 5 completed shifts\n- `upcoming_shifts` - Next 10 scheduled shifts\n- `notifications` - Unread notifications (last 20)\n- `business_stats` - Business statistics (admin/manager only)\n- `feature_flags` - Enabled feature flags\n- `system_status` - System status and announcements\n\n### Alternative Request Methods\n\nUsing headers:\n```\nX-Include-Piggyback: business_settings,user_preferences\n```\n\nUsing POST body:\n```json\n{\n  \"email\": \"user@example.com\",\n  \"password\": \"password\",\n  \"_include\": {\n    \"business_settings\": true,\n    \"user_preferences\": true\n  }\n}\n```\n\n### Server Recommendations\n\nFor mobile apps, the server automatically includes recommended data for login and profile \nrequests to optimize the user experience. Server recommendations include:\n- Suggested next actions based on user context\n- Setup reminders for incomplete profiles\n- Important notifications requiring attention\n- App usage tips and feature discovery\n\n### Piggyback Response Structure\n\nWhen piggyback data is included, responses contain an additional `_meta` section:\n```json\n{\n  \"user\": { /* primary response data */ },\n  \"_meta\": {\n    \"piggyback\": {\n      \"business_settings\": { /* business configuration */ },\n      \"user_preferences\": { /* user settings */ },\n      \"upcoming_shifts\": [ /* shift array */ ]\n    },\n    \"query_info\": {\n      \"included_data\": [\"business_settings\", \"user_preferences\", \"upcoming_shifts\"],\n      \"execution_time_ms\": 45.2,\n      \"cache_hits\": 2,\n      \"cache_misses\": 1\n    }\n  }\n}\n```\n\n## HTTP Protocol Enhancements\n\nThe API includes comprehensive HTTP protocol enhancements for optimal client performance:\n\n### Intelligent Caching\n- **ETags**: Content-based versioning for efficient cache validation\n- **Cache-Control**: Fine-grained cache behavior control\n- **Conditional Requests**: Support for If-None-Match headers (304 responses)\n- **Vary Headers**: Content negotiation awareness\n\n### Dynamic Navigation\n- **Next Actions**: Server-suggested user actions based on context\n- **Preload Hints**: Resources to preload for better performance\n- **Conditional Redirects**: Dynamic flow control based on user state\n- **Link Relations**: Standard HTTP Link headers for navigation\n\n### Performance Optimization\n- **Response Compression**: Automatic gzip/deflate compression\n- **Response Timing**: Server processing metrics\n- **Cache Status**: Hit/miss reporting for optimization\n- **Size Estimation**: Bandwidth usage awareness\n\n### Base Path and Versioning\n- All endpoint paths in this specification are relative. The `/api/v1` base path is provided by the `servers` section below.\n- When calling the API, combine the server URL with the relative path. For example: `https://dev.workforce.mangoapps.com/api/v1` + `/shifts` → `https://dev.workforce.mangoapps.com/api/v1/shifts`.\n- Do not include `/api/v1` in individual path keys; it is already defined at the server level for consistency across environments.\n\n### Client Hints & Reliability\n- **Rate Limiting**: Request quota and reset information (429 status)\n- **API Versioning**: Version compatibility information\n- **Deprecation Warnings**: Graceful API evolution notices\n- **Capability Detection**: Client feature detection\n- **Idempotency Support**: Duplicate request prevention with Idempotency-Key\n- **Conditional Requests**: 304 Not Modified for cached content\n\n### Example Enhanced Response Headers\n```http\nHTTP/1.1 200 OK\nContent-Type: application/json; charset=utf-8\nContent-Encoding: gzip\nETag: W/\"abc123-def456\"\nCache-Control: private, max-age=300, must-revalidate\nVary: Accept, Authorization, X-App-Platform\nLink: </api/v1/shifts?status=upcoming>; rel=\"preload\"; as=\"fetch\"\nX-Response-Time: 45.2ms\nX-Rate-Limit-Remaining: 98\nX-Rate-Limit-Reset: 1640995200\nX-Rate-Limit-Limit: 100\nX-Idempotency-Cached: false\nX-API-Version: 1.0\n```\n\nThese enhancements work seamlessly with the piggyback response system to provide:\n- **50-80% bandwidth reduction** with compression\n- **60-90% cache hit rates** with intelligent ETags\n- **2-3x faster navigation** with preload hints\n- **Reduced API calls** with conditional requests\n",
    "version": "1.0.0",
    "contact": {
      "name": "MangoApps API Support",
      "email": "api-support@workforce.mangoapps.com"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://api.workforce.mangoapps.com/api/v1",
      "description": "Production - Common API"
    },
    {
      "url": "https://dev.workforce.mangoapps.com/api/v1",
      "description": "Development - Common API"
    },
    {
      "url": "https://{business}.workforce.mangoapps.com/api/v1",
      "description": "Production - Business API",
      "variables": {
        "business": {
          "default": "demo",
          "description": "Business subdomain (e.g., officechat, acmecorp)",
          "examples": [
            "officechat",
            "acmecorp",
            "retailstore"
          ]
        }
      }
    },
    {
      "url": "https://{business}.dev.workforce.mangoapps.com/api/v1",
      "description": "Development - Business API",
      "variables": {
        "business": {
          "default": "demo",
          "description": "Business subdomain for development"
        }
      }
    }
  ],
  "security": [
    {
      "BearerAuth": []
    },
    {
      "ServiceAccountAuth": []
    }
  ],
  "paths": {
    "/changelog_entries": {
      "get": {
        "tags": [
          "Changelog"
        ],
        "summary": "List changelog entries",
        "description": "Retrieve a list of changelog entries. This endpoint is for internal use only \nand requires either internal API credentials or a changelog secret.\n",
        "security": [
          {
            "InternalAPI": []
          },
          {
            "ChangelogSecret": []
          }
        ],
        "responses": {
          "200": {
            "description": "List of changelog entries",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ChangelogEntry"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "example": 25
                        },
                        "published_count": {
                          "type": "integer",
                          "example": 20
                        },
                        "latest_version": {
                          "type": "string",
                          "example": "1.6"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "tags": [
          "Changelog"
        ],
        "summary": "Create changelog entry",
        "description": "Create a new changelog entry. This endpoint is for internal use only \nand requires either internal API credentials or a changelog secret.\n",
        "security": [
          {
            "InternalAPI": []
          },
          {
            "ChangelogSecret": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "changelog_entry"
                ],
                "properties": {
                  "changelog_entry": {
                    "$ref": "#/components/schemas/ChangelogEntryInput"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Changelog entry created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Changelog entry created successfully"
                    },
                    "data": {
                      "$ref": "#/components/schemas/ChangelogEntryResponse"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "message": {
                      "type": "string",
                      "example": "Failed to create changelog entry"
                    },
                    "errors": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "Title can't be blank",
                        "Content can't be blank"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/actioncue/webhooks/job_status/{job_uuid}": {
      "post": {
        "tags": [
          "ActionCue Webhooks"
        ],
        "summary": "Update job status via webhook",
        "description": "External processing systems use this endpoint to send job status updates.\nRequires HMAC-SHA256 signature for authentication.\n",
        "security": [],
        "parameters": [
          {
            "name": "job_uuid",
            "in": "path",
            "required": true,
            "description": "Unique identifier for the processing job",
            "schema": {
              "type": "string",
              "example": "550e8400-e29b-41d4-a716-446655440000"
            }
          },
          {
            "name": "X-ActionCue-Signature",
            "in": "header",
            "required": true,
            "description": "HMAC-SHA256 signature of request body",
            "schema": {
              "type": "string",
              "example": "sha256=a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "status"
                ],
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "pending",
                      "processing",
                      "completed",
                      "failed",
                      "cancelled"
                    ],
                    "description": "Current job status",
                    "example": "completed"
                  },
                  "progress": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "description": "Processing progress percentage",
                    "example": 95.5
                  },
                  "confidence_score": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1,
                    "description": "Overall confidence score",
                    "example": 0.85
                  },
                  "error_message": {
                    "type": "string",
                    "description": "Error message if status is failed",
                    "example": "Invalid file format"
                  },
                  "completed_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Completion timestamp",
                    "example": "2024-01-15T10:30:00Z"
                  },
                  "started_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Processing start timestamp",
                    "example": "2024-01-15T10:25:00Z"
                  },
                  "processing_time_ms": {
                    "type": "integer",
                    "description": "Processing time in milliseconds",
                    "example": 45000
                  },
                  "extracted_results": {
                    "type": "array",
                    "description": "Extracted form field data",
                    "items": {
                      "type": "object",
                      "properties": {
                        "field_id": {
                          "type": "string",
                          "example": "first_name"
                        },
                        "field_name": {
                          "type": "string",
                          "example": "First Name"
                        },
                        "extracted_value": {
                          "type": "string",
                          "example": "John"
                        },
                        "confidence": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 1,
                          "example": 0.95
                        }
                      }
                    }
                  },
                  "metadata": {
                    "type": "object",
                    "description": "Additional metadata from external system",
                    "additionalProperties": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Status updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "success"
                    },
                    "message": {
                      "type": "string",
                      "example": "Job status updated successfully"
                    },
                    "job_uuid": {
                      "type": "string",
                      "example": "550e8400-e29b-41d4-a716-446655440000"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "error"
                    },
                    "message": {
                      "type": "string",
                      "example": "Invalid webhook signature"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Job not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "error"
                    },
                    "message": {
                      "type": "string",
                      "example": "Processing job not found"
                    },
                    "job_uuid": {
                      "type": "string",
                      "example": "550e8400-e29b-41d4-a716-446655440000"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Invalid request data",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "error"
                    },
                    "message": {
                      "type": "string",
                      "example": "Configuration error: Status is required"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/actioncue/webhooks/batch_status": {
      "post": {
        "tags": [
          "ActionCue Webhooks"
        ],
        "summary": "Update multiple job statuses via webhook",
        "description": "External processing systems use this endpoint to send batch job status updates.\nRequires HMAC-SHA256 signature for authentication.\n",
        "security": [],
        "parameters": [
          {
            "name": "X-ActionCue-Signature",
            "in": "header",
            "required": true,
            "description": "HMAC-SHA256 signature of request body",
            "schema": {
              "type": "string",
              "example": "sha256=a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "jobs"
                ],
                "properties": {
                  "jobs": {
                    "type": "array",
                    "description": "Array of job status updates",
                    "items": {
                      "type": "object",
                      "required": [
                        "job_uuid",
                        "status"
                      ],
                      "properties": {
                        "job_uuid": {
                          "type": "string",
                          "example": "550e8400-e29b-41d4-a716-446655440000"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "processing",
                            "completed",
                            "failed",
                            "cancelled"
                          ],
                          "example": "completed"
                        },
                        "progress": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 100,
                          "example": 100
                        },
                        "confidence_score": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 1,
                          "example": 0.92
                        },
                        "error_message": {
                          "type": "string",
                          "example": null
                        },
                        "completed_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2024-01-15T10:30:00Z"
                        },
                        "processing_time_ms": {
                          "type": "integer",
                          "example": 45000
                        },
                        "extracted_results": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "field_id": {
                                "type": "string",
                                "example": "email"
                              },
                              "extracted_value": {
                                "type": "string",
                                "example": "john@example.com"
                              },
                              "confidence": {
                                "type": "number",
                                "example": 0.98
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Batch update processed successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "success"
                    },
                    "message": {
                      "type": "string",
                      "example": "Batch update processed"
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "job_uuid": {
                            "type": "string",
                            "example": "550e8400-e29b-41d4-a716-446655440000"
                          },
                          "status": {
                            "type": "string",
                            "example": "success"
                          },
                          "message": {
                            "type": "string",
                            "example": "Updated"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/actioncue/webhooks/health": {
      "get": {
        "tags": [
          "ActionCue Webhooks"
        ],
        "summary": "Health check for webhook endpoints",
        "description": "External systems can use this endpoint to verify webhook availability.\nNo authentication required for health checks.\n",
        "security": [],
        "responses": {
          "200": {
            "description": "Webhook service is healthy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "ok"
                    },
                    "timestamp": {
                      "type": "string",
                      "format": "date-time",
                      "example": "2024-01-15T10:30:00Z"
                    },
                    "version": {
                      "type": "string",
                      "example": "1.0"
                    },
                    "service": {
                      "type": "string",
                      "example": "ActionCue Webhooks"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/businesses/by_email": {
      "post": {
        "servers": [
          {
            "url": "https://api.workforce.mangoapps.com/api/v1",
            "description": "Production - Common API"
          },
          {
            "url": "https://dev.workforce.mangoapps.com/api/v1",
            "description": "Development - Common API"
          }
        ],
        "tags": [
          "Business Discovery"
        ],
        "summary": "Get businesses by user email",
        "description": "Find the tenants a user's email address can sign in to. This backs the\nworkspace picker after mobile's email-entry screen, and is the JSON twin\nof the web `POST /find_companies` flow.\n\nPrefer POST over the GET below: the query-string form writes the address\ninto access logs and error-tracker breadcrumbs.\n\nThrottled at 20 requests/minute per IP and 10/minute per email address.\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "example": "user@officechat.com"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Lookup completed. An empty `businesses` array means the address is\nunknown OR has no active tenants — the two are not distinguished.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BusinessDiscoveryResult"
                }
              }
            }
          },
          "400": {
            "description": "Missing or blank email",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      },
      "get": {
        "servers": [
          {
            "url": "https://api.workforce.mangoapps.com/api/v1",
            "description": "Production - Common API"
          },
          {
            "url": "https://dev.workforce.mangoapps.com/api/v1",
            "description": "Development - Common API"
          }
        ],
        "tags": [
          "Business Discovery"
        ],
        "summary": "Get businesses by user email (query-string form)",
        "deprecated": true,
        "description": "Kept for existing clients. Use `POST /businesses/by_email` instead — a\nquery string puts the email address into access logs and error-tracker\nbreadcrumbs. Identical behaviour and response otherwise.\n",
        "security": [],
        "parameters": [
          {
            "name": "email",
            "in": "query",
            "required": true,
            "description": "User's email address",
            "schema": {
              "type": "string",
              "format": "email",
              "example": "user@officechat.com"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Lookup completed. An empty `businesses` array means the address is\nunknown OR has no active tenants — the two are not distinguished.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BusinessDiscoveryResult"
                }
              }
            }
          },
          "400": {
            "description": "Missing or blank email",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      }
    },
    "/businesses/lookup": {
      "get": {
        "servers": [
          {
            "url": "https://api.workforce.mangoapps.com/api/v1",
            "description": "Production - Common API"
          },
          {
            "url": "https://dev.workforce.mangoapps.com/api/v1",
            "description": "Development - Common API"
          }
        ],
        "tags": [
          "Business Discovery"
        ],
        "summary": "Search businesses by company name",
        "description": "Search for businesses using partial or full company name matching.\nReturns up to 10 matching businesses. Matching ignores case AND\npunctuation on both sides — spaces, apostrophes, hyphens, periods —\nso `Oreilly`, `O'Reilly` and `oreilly auto parts` all find\n\"O'Reilly Auto Parts\". A term containing no letter or digit at all\n(e.g. `'''`) is rejected with `missing_company_name`, exactly like an\nabsent parameter. Useful for company name lookup and business\ndirectory features.\n",
        "security": [],
        "parameters": [
          {
            "name": "company_name",
            "in": "query",
            "required": true,
            "description": "Full or partial company name to search for",
            "schema": {
              "type": "string",
              "example": "Office Chat"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Businesses found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "businesses": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 456
                          },
                          "name": {
                            "type": "string",
                            "example": "Office Chat Solutions"
                          },
                          "subdomain": {
                            "type": "string",
                            "example": "officechat"
                          },
                          "logo_url": {
                            "type": "string",
                            "nullable": true,
                            "description": "Absolute logo URL, or null when unattached. Served from\nthe matched tenant's OWN host — this endpoint searches\nacross tenants, so it is not the requesting host.\n",
                            "example": "https://acme.workforce.mangoapps.com/rails/active_storage/blobs/redirect/…/logo.png"
                          },
                          "api_base_url": {
                            "type": "string",
                            "description": "Origin the client should send the subsequent login request\nto, and the disambiguator to show under each company name\nin a search list. Identical to the `api_base_url` that\n`/businesses/by_email` returns for the same business `id`\n— both endpoints render through one serializer. Already\ncarries the environment suffix (-dev / -qa / -staging;\nnone in production), so clients must not rebuild the host\nfrom `subdomain` themselves.\n",
                            "example": "https://officechat.workforce.mangoapps.com"
                          },
                          "timezone": {
                            "type": "string",
                            "example": "America/New_York"
                          },
                          "industry": {
                            "type": "string",
                            "description": "Tenant industry, or an empty string when unset — it is\nemitted as \"\" rather than omitted, so treat empty as\nabsent. Not a reliable subtitle; use `api_base_url`.\n",
                            "example": ""
                          }
                        }
                      }
                    },
                    "total_count": {
                      "type": "integer",
                      "example": 2,
                      "description": "Number of businesses found (max 10)"
                    },
                    "search_term": {
                      "type": "string",
                      "example": "Office Chat",
                      "description": "The search term used"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing company name parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No businesses found matching the search term",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/businesses/validate_subdomain": {
      "get": {
        "servers": [
          {
            "url": "https://api.workforce.mangoapps.com/api/v1",
            "description": "Production - Common API"
          },
          {
            "url": "https://dev.workforce.mangoapps.com/api/v1",
            "description": "Development - Common API"
          }
        ],
        "tags": [
          "Business Discovery"
        ],
        "summary": "Validate business subdomain exists",
        "description": "Check if a business subdomain is available and valid",
        "security": [],
        "parameters": [
          {
            "name": "subdomain",
            "in": "query",
            "required": true,
            "description": "Business subdomain to validate",
            "schema": {
              "type": "string",
              "example": "officechat"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Validation result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "exists": {
                      "type": "boolean",
                      "example": true
                    },
                    "subdomain": {
                      "type": "string",
                      "example": "officechat"
                    },
                    "business_name": {
                      "type": "string",
                      "example": "Office Chat Solutions"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing subdomain parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/auth/login": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "summary": "Login for mobile apps",
        "description": "Authenticate user and return access/refresh tokens.\n\n**Session Cookie Support:** In addition to returning API tokens, this endpoint \nalso establishes a Rails session and sets a session cookie (`_workforce_session_*`). \nThis allows the session cookie to be used for web page access without requiring \na separate web login. The session cookie is automatically set by the server and \nmanaged by HTTP clients.\n\n**Use Cases:**\n- Mobile apps: Use the access_token for API calls\n- Web views in mobile apps: The session cookie enables seamless web page access\n- Hybrid apps: Use either API tokens or session cookie as needed\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "password"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "example": "user@example.com"
                  },
                  "password": {
                    "type": "string",
                    "format": "password",
                    "example": "password123"
                  },
                  "otp_code": {
                    "type": "string",
                    "description": "Two-factor authentication code (required if 2FA enabled)",
                    "example": "123456"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Login successful",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "access_token": {
                      "type": "string",
                      "description": "JWT access token"
                    },
                    "refresh_token": {
                      "type": "string",
                      "description": "Refresh token for getting new access tokens"
                    },
                    "expires_in": {
                      "type": "integer",
                      "description": "Access token expiry in seconds"
                    },
                    "token_type": {
                      "type": "string",
                      "example": "Bearer"
                    },
                    "user": {
                      "$ref": "#/components/schemas/User"
                    },
                    "business": {
                      "$ref": "#/components/schemas/Business"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/PiggybackMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "two_factor_required",
                                "invalid_otp",
                                "invalid_credentials"
                              ]
                            },
                            "message": {
                              "type": "string"
                            },
                            "details": {
                              "type": "object",
                              "properties": {
                                "requires_2fa": {
                                  "type": "boolean"
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/auth/login_config": {
      "get": {
        "tags": [
          "Authentication",
          "Mobile"
        ],
        "summary": "Get login configuration for business",
        "description": "Returns complete login configuration for a business including available\nauthentication methods, SSO providers, security settings, and API endpoints.\nThis endpoint provides all information needed to render a mobile login screen.\n\nSSO Provider Support:\n- Multiple SSO providers of the same type are supported (e.g., multiple Google OAuth2 configs)\n- Each provider has a unique ID that should be used as the `provider_id` parameter\n- Use `channel_support` and `mobile_supported`/`web_supported` to filter providers appropriate for the client\n",
        "security": [],
        "parameters": [
          {
            "name": "business_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "Business ID to get login configuration for",
            "example": 123
          }
        ],
        "responses": {
          "200": {
            "description": "Login configuration retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LoginConfiguration"
                }
              }
            }
          },
          "404": {
            "description": "Business not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/auth/sso/initiate": {
      "post": {
        "tags": [
          "Authentication",
          "SSO",
          "Mobile"
        ],
        "summary": "Initiate SSO authentication flow",
        "description": "Initiates SSO authentication flow for mobile applications. Returns an authorization\nURL that should be opened in a web view or external browser. The mobile app should\nhandle the callback and extract the authorization code for token exchange.\n\nProvider Selection:\n1. Call `/auth/login_config` to get available SSO providers\n2. Display providers to user (multiple providers of same type may exist)\n3. Use the selected provider's `id` as the `provider_id` in this request\n4. For mobile, choose providers with `mobile_supported: true`\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "business_id",
                  "provider_id"
                ],
                "properties": {
                  "business_id": {
                    "type": "integer",
                    "description": "Business ID for SSO authentication",
                    "example": 123
                  },
                  "provider_id": {
                    "type": "string",
                    "description": "SSO provider ID from login configuration",
                    "example": "2"
                  },
                  "redirect_uri": {
                    "type": "string",
                    "description": "Optional redirect URI (HTTPS URL for Universal Links/App Links - Google OAuth does not support custom URL schemes)",
                    "example": "https://workforce.mangoapps.com/mobile/sso-callback"
                  },
                  "app_redirect_uri": {
                    "type": "string",
                    "description": "Desktop only. Where the callback page sends the browser once the IdP is\nfinished — never sent to the IdP itself. Accepted forms are a registered\ncustom scheme (`mangoappsdesktop://`, `mangoappsmessengerschema://`) or a\nloopback callback the app serves itself: `http://127.0.0.1:<port>/sso-callback`\nor `http://[::1]:<port>/sso-callback`, with no query, fragment, or credentials\nand a port in 1-65535. Anything else is dropped. Check the echoed\n`app_redirect_uri` in the response to see whether the value survived.\n",
                    "example": "http://127.0.0.1:52341/sso-callback"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "SSO flow initiated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SsoInitiateResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request parameters",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Business or SSO provider not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "SSO provider temporarily unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/auth/sso/exchange": {
      "post": {
        "tags": [
          "Authentication",
          "SSO",
          "Mobile"
        ],
        "summary": "Exchange authorization code for access tokens",
        "description": "Exchanges the authorization code received from SSO provider for access and refresh tokens.\nThis completes the SSO authentication flow and returns tokens that can be used for API access.\n\n**Session Cookie Support:** In addition to returning API tokens, this endpoint \nalso establishes a Rails session and sets a session cookie (`_workforce_session_*`). \nThis allows the session cookie to be used for web page access without requiring \na separate web login. The session cookie is automatically set by the server and \nincluded in the response headers - no client-side code changes are needed to receive it.\n\n**Use cases for session cookie:**\n- Web views in mobile apps: The session cookie enables seamless web page access\n- Hybrid apps: Use either API tokens or session cookie as needed\n- Native mobile + web integration: Access web content without re-login prompts\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "business_id",
                  "provider_id",
                  "code",
                  "state"
                ],
                "properties": {
                  "business_id": {
                    "type": "integer",
                    "description": "Business ID for SSO authentication",
                    "example": 123
                  },
                  "provider_id": {
                    "type": "string",
                    "description": "SSO provider ID from login configuration",
                    "example": "2"
                  },
                  "code": {
                    "type": "string",
                    "description": "Authorization code from SSO provider callback",
                    "example": "4/0AX4XfWjE..."
                  },
                  "state": {
                    "type": "string",
                    "description": "State parameter from SSO initiate response",
                    "example": "abc123def456"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "SSO authentication completed successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SsoExchangeResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid authorization code or state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "User not found or inactive",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "User not authorized for this business",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Business or SSO provider not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/auth/refresh": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "summary": "Refresh access token",
        "description": "Get a new access token using refresh token",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "refresh_token"
                ],
                "properties": {
                  "refresh_token": {
                    "type": "string",
                    "description": "Valid refresh token"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token refreshed successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "access_token": {
                      "type": "string"
                    },
                    "expires_in": {
                      "type": "integer"
                    },
                    "token_type": {
                      "type": "string",
                      "example": "Bearer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/auth/logout": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "summary": "Logout and revoke tokens",
        "description": "Revoke current access token and optionally refresh token",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "refresh_token": {
                    "type": "string",
                    "description": "Refresh token to revoke (optional)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Logged out successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Logged out successfully"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/auth/profile": {
      "get": {
        "tags": [
          "Authentication"
        ],
        "summary": "Get enhanced user profile",
        "description": "Retrieve comprehensive user profile including security info, preferences, \npermissions, and feature flags. This endpoint provides more detailed \ninformation than the basic /users/me endpoint.\n",
        "responses": {
          "200": {
            "description": "Profile retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user": {
                      "$ref": "#/components/schemas/EnhancedUser"
                    },
                    "business": {
                      "$ref": "#/components/schemas/Business"
                    },
                    "security": {
                      "$ref": "#/components/schemas/SecurityInfo"
                    },
                    "preferences": {
                      "type": "object",
                      "description": "User preferences and settings"
                    },
                    "permissions": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "User permissions"
                    },
                    "feature_flags": {
                      "type": "object",
                      "description": "Available feature flags for user"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "patch": {
        "tags": [
          "Authentication"
        ],
        "summary": "Update user profile",
        "description": "Update user profile information including preferences and notification settings",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "first_name": {
                    "type": "string",
                    "example": "John"
                  },
                  "last_name": {
                    "type": "string",
                    "example": "Doe"
                  },
                  "phone": {
                    "type": "string",
                    "example": "+1234567890"
                  },
                  "time_zone": {
                    "type": "string",
                    "example": "America/New_York"
                  },
                  "preferences": {
                    "type": "object",
                    "description": "User preferences to update"
                  },
                  "notification_preferences": {
                    "type": "object",
                    "description": "Notification preferences to update"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Profile updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Profile updated successfully"
                    },
                    "user": {
                      "$ref": "#/components/schemas/EnhancedUser"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/auth/change_password": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "summary": "Change user password",
        "description": "Change the current user's password with proper verification.\nOptionally revoke all other sessions for enhanced security.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "current_password",
                  "new_password",
                  "new_password_confirmation"
                ],
                "properties": {
                  "current_password": {
                    "type": "string",
                    "description": "Current password for verification"
                  },
                  "new_password": {
                    "type": "string",
                    "description": "New password (minimum 6 characters)"
                  },
                  "new_password_confirmation": {
                    "type": "string",
                    "description": "Confirmation of new password"
                  },
                  "revoke_other_sessions": {
                    "type": "boolean",
                    "default": false,
                    "description": "Whether to revoke all other active sessions"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Password changed successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Password changed successfully"
                    },
                    "revoked_sessions": {
                      "type": "boolean",
                      "description": "Whether other sessions were revoked"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/auth/password_reset/forgot_password": {
      "post": {
        "tags": [
          "Authentication",
          "Password Reset"
        ],
        "summary": "Request password reset",
        "description": "Initiate password reset process by sending a reset link to the user's email.\nReturns a generic success message regardless of whether the email exists (security best practice).\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "Email address of the account",
                    "example": "user@example.com"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Password reset email sent (or email doesn't exist - generic response for security)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "If an account exists with this email, you will receive password reset instructions shortly."
                    },
                    "email": {
                      "type": "string",
                      "example": "user@example.com"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        }
      }
    },
    "/auth/password_reset/validate_token": {
      "get": {
        "tags": [
          "Authentication",
          "Password Reset"
        ],
        "summary": "Validate password reset token",
        "description": "Check if a password reset token is valid and not expired.\nTokens expire after 6 hours.\n",
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Password reset token from email",
            "example": "abc123xyz789"
          }
        ],
        "responses": {
          "200": {
            "description": "Token is valid",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "valid": {
                      "type": "boolean",
                      "example": true
                    },
                    "email": {
                      "type": "string",
                      "example": "user@example.com"
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time",
                      "example": "2025-10-10T20:00:00Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Token is invalid or expired",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "invalid_or_expired_token",
                    "message": "Password reset token is invalid or has expired."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/auth/password_reset/reset_password": {
      "post": {
        "tags": [
          "Authentication",
          "Password Reset"
        ],
        "summary": "Reset password with token",
        "description": "Complete the password reset process using a valid reset token.\nOptionally revoke all existing sessions for enhanced security.\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "token",
                  "password",
                  "password_confirmation"
                ],
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "Password reset token from email",
                    "example": "abc123xyz789"
                  },
                  "password": {
                    "type": "string",
                    "format": "password",
                    "description": "New password (minimum 6 characters)",
                    "example": "newSecurePassword123"
                  },
                  "password_confirmation": {
                    "type": "string",
                    "format": "password",
                    "description": "Confirmation of new password",
                    "example": "newSecurePassword123"
                  },
                  "revoke_all_sessions": {
                    "type": "boolean",
                    "default": false,
                    "description": "Whether to revoke all existing sessions after password reset"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Password reset successful",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Your password has been reset successfully."
                    },
                    "email": {
                      "type": "string",
                      "example": "user@example.com"
                    },
                    "revoked_all_sessions": {
                      "type": "boolean",
                      "description": "Whether all sessions were revoked"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request (passwords don't match, too weak, etc.)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Token is invalid or expired",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/auth/sessions": {
      "get": {
        "tags": [
          "Authentication"
        ],
        "summary": "List active sessions",
        "description": "Retrieve a list of all active sessions (API tokens) for the current user.\nIncludes session details like device info, location, and last activity.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "Sessions retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Session"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "current_session_id": {
                          "type": "integer",
                          "description": "ID of the current session"
                        },
                        "total_active_sessions": {
                          "type": "integer",
                          "description": "Total number of active sessions"
                        }
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/PaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/auth/sessions/current": {
      "get": {
        "tags": [
          "Authentication"
        ],
        "summary": "Get current session details",
        "description": "Retrieve detailed information about the current active session",
        "responses": {
          "200": {
            "description": "Current session retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "session": {
                      "$ref": "#/components/schemas/DetailedSession"
                    },
                    "is_current_session": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/auth/sessions/revoke_all": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "summary": "Revoke all other sessions",
        "description": "Revoke all active sessions except the current one. Useful for security \nwhen a user suspects their account may be compromised.\n",
        "responses": {
          "200": {
            "description": "Other sessions revoked successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Successfully revoked 3 other sessions"
                    },
                    "revoked_count": {
                      "type": "integer",
                      "description": "Number of sessions revoked"
                    },
                    "current_session_preserved": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/auth/sessions/{id}": {
      "get": {
        "tags": [
          "Authentication"
        ],
        "summary": "Get session details",
        "description": "Retrieve detailed information about a specific session",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "Session ID"
          }
        ],
        "responses": {
          "200": {
            "description": "Session details retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "session": {
                      "$ref": "#/components/schemas/DetailedSession"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Authentication"
        ],
        "summary": "Revoke specific session",
        "description": "Revoke a specific session by ID. Cannot revoke the current session - \nuse the logout endpoint instead.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "Session ID to revoke"
          }
        ],
        "responses": {
          "200": {
            "description": "Session revoked successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Session revoked successfully"
                    },
                    "revoked_session": {
                      "$ref": "#/components/schemas/Session"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Cannot revoke current session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/auth/2fa/status": {
      "get": {
        "tags": [
          "Two-Factor Authentication"
        ],
        "summary": "Get 2FA status",
        "description": "Retrieve the current two-factor authentication status and configuration",
        "responses": {
          "200": {
            "description": "2FA status retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "two_factor_enabled": {
                      "type": "boolean",
                      "description": "Whether 2FA is currently enabled"
                    },
                    "two_factor_status": {
                      "type": "string",
                      "enum": [
                        "disabled",
                        "pending_setup",
                        "pending_confirmation",
                        "enabled"
                      ],
                      "description": "Current 2FA setup status"
                    },
                    "setup_required": {
                      "type": "boolean",
                      "description": "Whether 2FA setup is required by organization policy"
                    },
                    "backup_codes_count": {
                      "type": "integer",
                      "description": "Number of remaining backup codes"
                    },
                    "setup_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "When 2FA setup was initiated"
                    },
                    "confirmed_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "When 2FA was confirmed and enabled"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/auth/2fa/setup": {
      "post": {
        "tags": [
          "Two-Factor Authentication"
        ],
        "summary": "Setup 2FA",
        "description": "Initiate two-factor authentication setup. Returns QR code URI and backup codes.\nRequires current password for verification.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "current_password"
                ],
                "properties": {
                  "current_password": {
                    "type": "string",
                    "description": "Current password for verification"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "2FA setup initiated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Two-factor authentication setup initiated"
                    },
                    "qr_code_uri": {
                      "type": "string",
                      "description": "QR code URI for authenticator apps"
                    },
                    "secret": {
                      "type": "string",
                      "description": "Secret key for manual entry"
                    },
                    "backup_codes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Backup codes for account recovery"
                    },
                    "next_step": {
                      "type": "string",
                      "description": "Instructions for next step"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "2FA already enabled or invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/auth/2fa/verify": {
      "post": {
        "tags": [
          "Two-Factor Authentication"
        ],
        "summary": "Verify and enable 2FA",
        "description": "Complete 2FA setup by verifying a code from the authenticator app.\nThis enables 2FA for the user account.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "code"
                ],
                "properties": {
                  "code": {
                    "type": "string",
                    "description": "6-digit verification code from authenticator app",
                    "example": "123456"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "2FA enabled successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Two-factor authentication enabled successfully"
                    },
                    "two_factor_enabled": {
                      "type": "boolean",
                      "example": true
                    },
                    "backup_codes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Backup codes for account recovery"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid code or 2FA not setup",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/auth/2fa/disable": {
      "post": {
        "tags": [
          "Two-Factor Authentication"
        ],
        "summary": "Disable 2FA",
        "description": "Disable two-factor authentication for the user account.\nRequires current password and either a 2FA code or backup code.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "current_password"
                ],
                "properties": {
                  "current_password": {
                    "type": "string",
                    "description": "Current password for verification"
                  },
                  "code": {
                    "type": "string",
                    "description": "6-digit verification code from authenticator app",
                    "example": "123456"
                  },
                  "backup_code": {
                    "type": "string",
                    "description": "Backup code (alternative to verification code)",
                    "example": "a1b2c3d4"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "2FA disabled successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Two-factor authentication disabled successfully"
                    },
                    "two_factor_enabled": {
                      "type": "boolean",
                      "example": false
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "2FA not enabled or invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/auth/2fa/backup_codes": {
      "get": {
        "tags": [
          "Two-Factor Authentication"
        ],
        "summary": "Get backup codes",
        "description": "Retrieve current backup codes for 2FA recovery.\nRequires current password for security.\n",
        "parameters": [
          {
            "name": "current_password",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Current password for verification"
          }
        ],
        "responses": {
          "200": {
            "description": "Backup codes retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "backup_codes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Current backup codes"
                    },
                    "codes_count": {
                      "type": "integer",
                      "description": "Number of backup codes"
                    },
                    "warning": {
                      "type": "string",
                      "description": "Security warning about backup codes"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "2FA not enabled",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/auth/2fa/regenerate_backup_codes": {
      "post": {
        "tags": [
          "Two-Factor Authentication"
        ],
        "summary": "Regenerate backup codes",
        "description": "Generate new backup codes, invalidating all previous codes.\nRequires current password for security.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "current_password"
                ],
                "properties": {
                  "current_password": {
                    "type": "string",
                    "description": "Current password for verification"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Backup codes regenerated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Backup codes regenerated successfully"
                    },
                    "backup_codes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "New backup codes"
                    },
                    "warning": {
                      "type": "string",
                      "description": "Warning about old codes being invalidated"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "2FA not enabled",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/auth/passwordless/request": {
      "post": {
        "tags": [
          "Passwordless Authentication"
        ],
        "summary": "Request passwordless authentication",
        "description": "Request a passwordless authentication token via email or SMS.\nSupports magic links, email verification codes, and SMS codes.\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "User's email address",
                    "example": "user@example.com"
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "magic_link",
                      "verification_code",
                      "sms_code"
                    ],
                    "default": "magic_link",
                    "description": "Type of passwordless authentication",
                    "example": "magic_link"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Authentication request sent successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Magic link sent to your email address"
                    },
                    "type": {
                      "type": "string",
                      "example": "magic_link"
                    },
                    "expires_in": {
                      "type": "string",
                      "description": "Human-readable expiration time",
                      "example": "1 hour"
                    },
                    "token_id": {
                      "type": "integer",
                      "description": "Token ID for debugging (development only)",
                      "example": 123
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "422": {
            "description": "No account found or validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/auth/passwordless/verify": {
      "post": {
        "tags": [
          "Passwordless Authentication"
        ],
        "summary": "Verify passwordless authentication token",
        "description": "Verify a passwordless authentication token and receive API access tokens.\nWorks with magic link tokens, email codes, and SMS codes.\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "token"
                ],
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "Authentication token from email/SMS",
                    "example": "abc123def456"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Authentication successful",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "access_token": {
                      "type": "string",
                      "description": "API access token"
                    },
                    "refresh_token": {
                      "type": "string",
                      "description": "Token refresh token"
                    },
                    "expires_in": {
                      "type": "integer",
                      "description": "Token expiration in seconds",
                      "example": 3600
                    },
                    "token_type": {
                      "type": "string",
                      "example": "Bearer"
                    },
                    "authentication_method": {
                      "type": "string",
                      "example": "passwordless"
                    },
                    "passwordless_type": {
                      "type": "string",
                      "example": "magic_link"
                    },
                    "user": {
                      "$ref": "#/components/schemas/User"
                    },
                    "business": {
                      "$ref": "#/components/schemas/Business"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid or expired token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/auth/passwordless/status": {
      "get": {
        "tags": [
          "Passwordless Authentication"
        ],
        "summary": "Get passwordless authentication status",
        "description": "Get information about recent passwordless authentication attempts\nand system configuration.\n",
        "responses": {
          "200": {
            "description": "Status retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "passwordless_enabled": {
                      "type": "boolean",
                      "example": true
                    },
                    "recent_tokens": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PasswordlessToken"
                      }
                    },
                    "supported_types": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "magic_link",
                        "verification_code",
                        "sms_code"
                      ]
                    },
                    "rate_limit_info": {
                      "type": "object",
                      "properties": {
                        "max_requests_per_5_minutes": {
                          "type": "integer",
                          "example": 3
                        },
                        "current_period_requests": {
                          "type": "integer",
                          "example": 1
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/auth/tokens": {
      "get": {
        "tags": [
          "API Token Management"
        ],
        "summary": "List personal API tokens",
        "description": "Retrieve a list of personal API tokens for the current user.\nExcludes session tokens and service account tokens.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "Tokens retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PersonalApiToken"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "total_tokens": {
                          "type": "integer",
                          "description": "Total number of tokens"
                        },
                        "active_tokens": {
                          "type": "integer",
                          "description": "Number of active tokens"
                        },
                        "available_scopes": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "Available scopes for personal tokens"
                        }
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/PaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "tags": [
          "API Token Management"
        ],
        "summary": "Create personal API token",
        "description": "Create a new personal API token with specified scopes and expiration.\nRequires current password for security verification.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "current_password"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Token name/description",
                    "example": "My Automation Token",
                    "maxLength": 100
                  },
                  "current_password": {
                    "type": "string",
                    "description": "Current password for verification"
                  },
                  "scopes": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Requested scopes (filtered to employee-safe scopes)",
                    "example": [
                      "read:own_profile",
                      "read:own_shifts",
                      "write:own_attendance"
                    ]
                  },
                  "expires_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Optional expiration date",
                    "example": "2024-12-31T23:59:59Z"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Token created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "API token created successfully"
                    },
                    "token": {
                      "$ref": "#/components/schemas/DetailedPersonalApiToken"
                    },
                    "warning": {
                      "type": "string",
                      "example": "Store this token securely. You will not be able to see it again."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "description": "Token limit exceeded or validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/auth/tokens/scopes": {
      "get": {
        "tags": [
          "API Token Management"
        ],
        "summary": "Get available token scopes",
        "description": "Retrieve list of available scopes for personal API tokens\nwith descriptions and categories.\n",
        "responses": {
          "200": {
            "description": "Scopes retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "available_scopes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string",
                            "example": "read:own_profile"
                          },
                          "description": {
                            "type": "string",
                            "example": "View your profile information"
                          },
                          "category": {
                            "type": "string",
                            "example": "profile"
                          }
                        }
                      }
                    },
                    "scope_categories": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string"
                      },
                      "example": {
                        "profile": "User profile and preferences",
                        "shifts": "Shift and schedule information"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/auth/tokens/{id}": {
      "get": {
        "tags": [
          "API Token Management"
        ],
        "summary": "Get token details",
        "description": "Retrieve detailed information about a specific personal API token",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "Token ID"
          }
        ],
        "responses": {
          "200": {
            "description": "Token details retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "token": {
                      "$ref": "#/components/schemas/DetailedPersonalApiToken"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "API Token Management"
        ],
        "summary": "Delete personal API token",
        "description": "Delete (revoke) a personal API token. Requires current password for security.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "Token ID to delete"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "current_password"
                ],
                "properties": {
                  "current_password": {
                    "type": "string",
                    "description": "Current password for verification"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token deleted successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "API token deleted successfully"
                    },
                    "deleted_token": {
                      "$ref": "#/components/schemas/PersonalApiToken"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/auth/tokens/{id}/rotate": {
      "patch": {
        "tags": [
          "API Token Management"
        ],
        "summary": "Rotate API token",
        "description": "Generate a new token value for an existing API token while preserving\nall other settings. Requires current password for security.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "Token ID to rotate"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "current_password"
                ],
                "properties": {
                  "current_password": {
                    "type": "string",
                    "description": "Current password for verification"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token rotated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "API token rotated successfully"
                    },
                    "token": {
                      "$ref": "#/components/schemas/DetailedPersonalApiToken"
                    },
                    "old_token": {
                      "type": "object",
                      "description": "Information about the old token (without token value)"
                    },
                    "warning": {
                      "type": "string",
                      "example": "Update your applications with the new token. The old token is no longer valid."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/auth/tokens/{id}/usage": {
      "get": {
        "tags": [
          "API Token Management"
        ],
        "summary": "Get token usage statistics",
        "description": "Retrieve usage statistics and analytics for a personal API token.\nNote: Detailed usage tracking is being implemented.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "Token ID"
          }
        ],
        "responses": {
          "200": {
            "description": "Usage statistics retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "usage": {
                      "$ref": "#/components/schemas/TokenUsage"
                    },
                    "note": {
                      "type": "string",
                      "example": "Usage tracking is being implemented and will provide detailed analytics soon."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/auth/impersonate": {
      "post": {
        "tags": [
          "Impersonation"
        ],
        "summary": "Create impersonation token",
        "description": "Create a short-lived impersonation token to act on behalf of a specific user.\nRequires a service account with `impersonate` or `admin` scope.\n\nImpersonation tokens allow service accounts to access user-specific endpoints\n(e.g., `/api/v1/users/me`, `/api/v1/leave_balances`) as if authenticated as that user.\n\nAll access using impersonation tokens is automatically logged for audit purposes.\n",
        "security": [
          {
            "BearerAuth": []
          },
          {
            "ServiceAccountAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "user_id"
                ],
                "properties": {
                  "user_id": {
                    "type": "integer",
                    "description": "ID of the user to impersonate",
                    "example": 123
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Impersonation token created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "token": {
                          "type": "string",
                          "description": "Impersonation token (use in Authorization header)",
                          "example": "a1b2c3d4e5f6..."
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "Token expiration time",
                          "example": "2024-01-15T14:30:00Z"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Service account does not have impersonate scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "forbidden",
                    "message": "Service account with 'impersonate' or 'admin' scope required"
                  }
                }
              }
            }
          },
          "404": {
            "description": "User not found in current business",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "user_not_found",
                    "message": "User not found in current business"
                  }
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Impersonation"
        ],
        "summary": "List or get impersonation token details",
        "description": "If called with a service account token, returns a list of active impersonation tokens\ncreated by that service account.\n\nIf called with an impersonation token, returns details about the current impersonation\nsession (impersonated user, impersonator, expiration, etc.).\n",
        "security": [
          {
            "BearerAuth": []
          },
          {
            "ServiceAccountAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Impersonation tokens retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "description": "List of tokens (when called with service account)",
                      "properties": {
                        "success": {
                          "type": "boolean",
                          "example": true
                        },
                        "data": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "token": {
                                "type": "string",
                                "description": "Impersonation token"
                              },
                              "impersonated_user_id": {
                                "type": "integer",
                                "description": "ID of the user being impersonated"
                              },
                              "expires_at": {
                                "type": "string",
                                "format": "date-time",
                                "description": "Token expiration time"
                              }
                            }
                          }
                        }
                      }
                    },
                    {
                      "type": "object",
                      "description": "Current token details (when called with impersonation token)",
                      "properties": {
                        "success": {
                          "type": "boolean",
                          "example": true
                        },
                        "data": {
                          "type": "object",
                          "properties": {
                            "token": {
                              "type": "string",
                              "description": "Impersonation token"
                            },
                            "impersonated_user_id": {
                              "type": "integer",
                              "description": "ID of the user being impersonated"
                            },
                            "impersonator_user_id": {
                              "type": "integer",
                              "description": "ID of the service account user who created this token"
                            },
                            "expires_at": {
                              "type": "string",
                              "format": "date-time",
                              "description": "Token expiration time"
                            },
                            "active": {
                              "type": "boolean",
                              "description": "Whether the token is currently active (not expired or revoked)"
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Access denied",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/auth/impersonate/{token}": {
      "delete": {
        "tags": [
          "Impersonation"
        ],
        "summary": "Revoke impersonation token",
        "description": "Revoke an impersonation token. Only the service account that created the token\ncan revoke it.\n",
        "security": [
          {
            "BearerAuth": []
          },
          {
            "ServiceAccountAuth": []
          }
        ],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The impersonation token to revoke"
          }
        ],
        "responses": {
          "200": {
            "description": "Token revoked successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "message": {
                          "type": "string",
                          "example": "Impersonation token revoked"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Access denied",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Token not found or not owned by this service account",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/home": {
      "get": {
        "tags": [
          "Home"
        ],
        "summary": "Get home screen data",
        "description": "Returns comprehensive home screen data including dashboard widgets, \nuser profile, business context, notifications, and quick stats.\n\nThis endpoint provides all data needed to render a complete mobile \nhome screen in a single API call.\n\n### Widget System\nThe dashboard includes configurable widgets based on:\n- User role (employee, manager, admin, super_admin)\n- Enabled business features\n- Active marketplace apps\n\n### Performance Metrics\nIncludes user performance data such as:\n- Weekly hours worked\n- Attendance rate and punctuality\n- Completed shifts count\n\n### Real-time Data\n- Current attendance status\n- Upcoming shifts\n- Unread notifications\n- Quick action shortcuts\n",
        "responses": {
          "200": {
            "description": "Home screen data retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/User"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "onboarding_completed": {
                              "type": "boolean",
                              "description": "Whether user has completed onboarding"
                            },
                            "onboarding_progress": {
                              "type": "number",
                              "minimum": 0,
                              "maximum": 100,
                              "description": "Onboarding completion percentage"
                            },
                            "preferences": {
                              "type": "object",
                              "description": "User preferences and settings"
                            }
                          }
                        }
                      ]
                    },
                    "business": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Business"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "settings": {
                              "type": "object",
                              "properties": {
                                "shifts_scheduling_enabled": {
                                  "type": "boolean"
                                },
                                "time_attendance_enabled": {
                                  "type": "boolean"
                                },
                                "leave_management_enabled": {
                                  "type": "boolean"
                                },
                                "timesheets_enabled": {
                                  "type": "boolean"
                                },
                                "skills_certifications_enabled": {
                                  "type": "boolean"
                                },
                                "enabled_marketplace_apps": {
                                  "type": "array",
                                  "items": {
                                    "type": "string"
                                  },
                                  "description": "List of enabled marketplace app slugs"
                                },
                                "time_clock_kiosk_enabled": {
                                  "type": "boolean",
                                  "description": "Tenant runs a shared Time Clock Kiosk, so punching moved off personal devices onto the tablet at the site. Clients must stop offering Clock In / Clock Out and show the kiosk card from current_status.kiosk instead.\n"
                                },
                                "business_hours": {
                                  "type": "object",
                                  "additionalProperties": {
                                    "type": "object",
                                    "properties": {
                                      "open": {
                                        "type": "string",
                                        "format": "time"
                                      },
                                      "close": {
                                        "type": "string",
                                        "format": "time"
                                      },
                                      "closed": {
                                        "type": "boolean"
                                      }
                                    }
                                  }
                                },
                                "week_start_day": {
                                  "type": "string",
                                  "enum": [
                                    "sunday",
                                    "monday",
                                    "tuesday",
                                    "wednesday",
                                    "thursday",
                                    "friday",
                                    "saturday"
                                  ],
                                  "default": "monday"
                                }
                              }
                            }
                          }
                        }
                      ]
                    },
                    "dashboard": {
                      "type": "object",
                      "properties": {
                        "widgets": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "name": {
                                "type": "string",
                                "description": "Widget identifier"
                              },
                              "title": {
                                "type": "string",
                                "description": "Display title"
                              },
                              "description": {
                                "type": "string",
                                "description": "Widget description"
                              },
                              "category": {
                                "type": "string",
                                "enum": [
                                  "platform",
                                  "core_app",
                                  "marketplace_app"
                                ]
                              },
                              "icon": {
                                "type": "string",
                                "description": "FontAwesome icon class"
                              },
                              "size": {
                                "type": "string",
                                "description": "Bootstrap grid size class"
                              },
                              "type": {
                                "type": "string",
                                "description": "Widget category type"
                              },
                              "priority": {
                                "type": "integer",
                                "description": "Display priority (lower = higher priority)"
                              },
                              "data": {
                                "type": "object",
                                "description": "Widget-specific data content following the Simple Widget Specification. Contains both structured data and markdown content for flexible mobile rendering.\n\n**Core Structure**:\n- Original structured data (actions, shifts, notifications, etc.)\n- `markdown_summary`: Rich markdown content for mobile rendering\n- `display_hint`: Layout hint (grid, list, feed, card)\n\n**Platform Widgets**:\n- `quick_actions`: { actions: [{ title, path, icon, variant }], markdown_summary, display_hint: 'grid' }\n- `notifications`: { recent: [], unread_count: 0, markdown_summary, display_hint: 'feed' }\n- `user_profile_summary`: { greeting, weekly_hours, current_status, markdown_summary, display_hint: 'card' }\n\n**Core App Widgets**:\n- `upcoming_shifts`: { shifts: [], user_role, show_team_view, markdown_summary, display_hint: 'list' }\n- `quick_attendance`: { current_status: { status, shift: { id, date, start_time, end_time, location, duration, is_ad_hoc }, clocked_in_at, duration, can_clock_in }, today_hours, can_clock_in, markdown_summary, display_hint: 'card' }\n- `leave_requests`: { leave_requests: [{ id, start_date (YYYY-MM-DD), end_date (YYYY-MM-DD), formatted_dates, duration_days, status_badge: { class, text }, leave_type: { name, color }, reason, created_at, url }], leave_balance: [], quick_actions: [], user_role, show_manager_view, markdown_summary, display_hint: 'list' }\n\n**Marketplace App Widgets**:\n- `epms_*`: Performance management data with markdown_summary and display_hint\n- `training_connect_*`: Training data with markdown_summary and display_hint\n- `okr_progress`: OKR data with markdown_summary and display_hint\n\nThis hybrid approach provides both structured data for custom rendering and markdown content for quick implementation.\n",
                                "additionalProperties": true,
                                "properties": {
                                  "markdown_summary": {
                                    "type": "string",
                                    "description": "Rich markdown content for mobile rendering. Contains formatted text with emojis, links, and styling cues.",
                                    "example": "## Quick Actions\n\n🔵 **[Clock In](/attendance/clock_in)**\n📅 **[My Schedule](/shifts/my_shifts)**\n"
                                  },
                                  "display_hint": {
                                    "type": "string",
                                    "enum": [
                                      "grid",
                                      "list",
                                      "feed",
                                      "card"
                                    ],
                                    "description": "Layout hint for mobile apps:\n- grid: 2-3 column layout for actions/buttons\n- list: Single column with dividers for items\n- feed: Card-based with timestamps for activity\n- card: Full-width summary display\n"
                                  }
                                },
                                "example": {
                                  "actions": [
                                    {
                                      "title": "Clock In",
                                      "path": "/attendance/clock_in",
                                      "icon": "fas fa-play",
                                      "variant": "primary"
                                    },
                                    {
                                      "title": "My Schedule",
                                      "path": "/shifts/my_shifts",
                                      "icon": "fas fa-calendar",
                                      "variant": "outline-info"
                                    }
                                  ]
                                }
                              },
                              "last_updated": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "refresh_interval": {
                                "type": "integer",
                                "description": "Recommended refresh interval in seconds"
                              },
                              "data_schema": {
                                "type": "object",
                                "description": "JSON Schema describing the expected structure of the 'data' field.\nHelps mobile developers understand widget data format and handle new widgets gracefully.\n",
                                "additionalProperties": true
                              },
                              "version": {
                                "type": "string",
                                "description": "Widget version for mobile app compatibility.\nIncrement when widget data structure changes significantly.\n",
                                "example": "1.2"
                              }
                            }
                          }
                        },
                        "widget_count": {
                          "type": "integer",
                          "description": "Total number of active widgets"
                        },
                        "performance_metrics": {
                          "type": "object",
                          "properties": {
                            "current_week": {
                              "type": "object",
                              "properties": {
                                "hours_worked": {
                                  "type": "number",
                                  "format": "float"
                                },
                                "shifts_completed": {
                                  "type": "integer"
                                },
                                "on_time_rate": {
                                  "type": "number",
                                  "format": "float",
                                  "minimum": 0,
                                  "maximum": 100
                                },
                                "attendance_rate": {
                                  "type": "number",
                                  "format": "float",
                                  "minimum": 0,
                                  "maximum": 100
                                }
                              }
                            },
                            "current_month": {
                              "type": "object",
                              "properties": {
                                "total_shifts": {
                                  "type": "integer"
                                },
                                "hours_worked": {
                                  "type": "number",
                                  "format": "float"
                                },
                                "performance_score": {
                                  "type": "number",
                                  "format": "float",
                                  "minimum": 0,
                                  "maximum": 100
                                }
                              }
                            }
                          }
                        },
                        "role_context": {
                          "type": "object",
                          "properties": {
                            "current_role": {
                              "type": "string",
                              "enum": [
                                "employee",
                                "manager",
                                "admin",
                                "super_admin"
                              ]
                            },
                            "permissions": {
                              "type": "array",
                              "items": {
                                "type": "string"
                              },
                              "description": "User's permissions in current business"
                            },
                            "available_actions": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "name": {
                                    "type": "string"
                                  },
                                  "label": {
                                    "type": "string"
                                  },
                                  "icon": {
                                    "type": "string"
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "attendance_policy": {
                      "type": "object",
                      "nullable": true,
                      "description": "The tenant's clock-in policy, resolved for the calling user, so a client can render the clock-in gate and know which verifications to gather without re-implementing the rules. `null` when Time & Attendance is disabled for the business or the business has no security setting yet. `settings` mirrors every punch-behaviour field on /admin/attendance_security/settings, using the same names. Kiosk PIN derivation (`default_pin_rule`, `default_pin_custom_value`) is deliberately never returned; the caller's own PIN-free kiosk credentials ship in `current_status.kiosk` instead.\n",
                      "properties": {
                        "clock_in_gate": {
                          "type": "object",
                          "properties": {
                            "active": {
                              "type": "boolean",
                              "description": "The decision, and the only field a client should branch on. Already folds in scope, admin exemption, approved leave and \"already clocked in\". Comes from the same resolver the web gate uses, so the two cannot drift.\n"
                            },
                            "mode": {
                              "type": "string",
                              "nullable": true,
                              "enum": [
                                "block",
                                "banner"
                              ],
                              "description": "How to present the gate — `block` redirects until clocked in, `banner` reminds without blocking. `null` when the gate is disabled for the tenant.\n"
                            },
                            "enabled": {
                              "type": "boolean",
                              "description": "Raw tenant setting (is the gate switched on at all). Informational — re-deriving the gate from this will drift from `active`.\n"
                            },
                            "scope": {
                              "type": "string",
                              "enum": [
                                "punch_eligible",
                                "all_users"
                              ],
                              "description": "Raw tenant setting for who the gate applies to. Informational."
                            }
                          }
                        },
                        "settings": {
                          "type": "object",
                          "description": "Straight projection of the tenant's attendance security settings.",
                          "properties": {
                            "allow_remote_clock_in": {
                              "type": "boolean",
                              "description": "Offer a \"Working remotely\" option in the location picker."
                            },
                            "allow_offsite_clock_in": {
                              "type": "boolean",
                              "description": "Offer an \"Off-site / traveling\" option with an optional note."
                            },
                            "require_shift_for_clock_in": {
                              "type": "boolean",
                              "description": "A punch is only accepted against a scheduled shift. When false, an ad-hoc shift is created for an unscheduled clock-in.\n"
                            },
                            "require_location_verification": {
                              "type": "boolean",
                              "description": "Master switch for geographic verification. Each location must also enable geofencing for enforcement to take effect.\n"
                            },
                            "require_device_verification": {
                              "type": "boolean"
                            },
                            "require_photo_verification": {
                              "type": "boolean",
                              "description": "Employee must capture a photo when clocking in or out."
                            },
                            "enforce_ip_validation_for_clock_actions": {
                              "type": "boolean",
                              "description": "Master switch for the IP check. When false, no IP validation happens at all regardless of `ip_validation_mode`.\n"
                            },
                            "ip_validation_mode": {
                              "type": "string",
                              "enum": [
                                "passive",
                                "warning",
                                "strict"
                              ],
                              "description": "Only meaningful when `enforce_ip_validation_for_clock_actions` is true. `passive` records only, `warning` flags for review, `strict` refuses the punch from an unapproved IP.\n"
                            },
                            "enforce_time_window_restrictions": {
                              "type": "boolean",
                              "description": "Apply the three buffers below. Note the window check is skipped entirely for ad-hoc shifts even when this is true, so do not present a window on a punch against one.\n"
                            },
                            "early_clock_in_buffer_minutes": {
                              "type": "integer",
                              "description": "Minutes before scheduled shift start that clock-in is allowed. `0` requires the exact start time.\n"
                            },
                            "late_clock_in_buffer_minutes": {
                              "type": "integer",
                              "description": "Minutes after shift start before a clock-in is flagged as outside the window. `0` requires the exact start time.\n"
                            },
                            "late_clock_out_buffer_minutes": {
                              "type": "integer",
                              "description": "Minutes after shift end before a clock-out is flagged. `0` disables the check — late clock-outs are never flagged — rather than meaning zero tolerance, which is the opposite of how `0` reads on the two clock-in buffers above. Early clock-out is never restricted at all.\n"
                            },
                            "enable_auto_clock_out": {
                              "type": "boolean",
                              "description": "The server closes forgotten punches. Surfaced so a client can warn the employee rather than have an auto clock-out look like a lost shift.\n"
                            },
                            "auto_clock_out_max_hours": {
                              "type": "integer",
                              "description": "Hours after clock-in at which the punch is closed automatically."
                            },
                            "auto_clock_out_after_shift_end_minutes": {
                              "type": "integer",
                              "description": "For scheduled shifts, minutes after shift end before auto clock-out."
                            },
                            "enable_clock_out_reminders": {
                              "type": "boolean",
                              "description": "Send a reminder before the shift ends. Independent of `enable_auto_clock_out` — the sweep selects the union of the two flags, so a tenant may receive reminders without the server ever auto-closing a punch. Do not present this as a sub-setting of auto clock-out.\n"
                            },
                            "effective_clock_out_reminder_minutes": {
                              "type": "integer",
                              "minimum": 5,
                              "description": "Minutes before scheduled shift end that the reminder is sent. This is the stored `clock_out_reminder_minutes_before` clamped to the reminder sweep's cron floor, not the raw column — a row holding a lower value (or none) still reminds at the floor, so this is the only number safe to show an employee.\n"
                            }
                          }
                        }
                      }
                    },
                    "broadcast_alerts": {
                      "type": "object",
                      "nullable": true,
                      "description": "Unread/response badge counts for the Broadcast & Alerts app. `null` when the app is not enabled for the business. Each `count` matches the `all` value the corresponding list API returns in `meta.segment_counts`, so the home badge can't drift from the list. A `count` may be `null` if its computation fails (the node is still returned).\n",
                      "properties": {
                        "broadcast": {
                          "type": "object",
                          "properties": {
                            "count": {
                              "type": "integer",
                              "nullable": true,
                              "description": "Received broadcasts the user has not read"
                            }
                          }
                        },
                        "alert": {
                          "type": "object",
                          "properties": {
                            "count": {
                              "type": "integer",
                              "nullable": true,
                              "description": "Alert responses (acknowledge / safety check-in) the user owes"
                            },
                            "items": {
                              "type": "array",
                              "maxItems": 10,
                              "description": "The response-pending alerts the count covers — ack-required alerts the user has not acknowledged, or safety-check-in alerts the user has not responded to — newest published first, capped at 10 (a preview; the full total is in `count`).\n",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "integer"
                                  },
                                  "title": {
                                    "type": "string"
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "forms": {
                      "type": "object",
                      "nullable": true,
                      "description": "Forms pending-approval badge. `null` unless the Forms app is enabled for the business AND the caller may approve/reject submissions (admin / manager / Forms App Admin) — the same gate as GET /api/v1/forms/approvals. Returned `null` (never omitted erroneously) if the count computation fails, so the home payload is never dropped.\n",
                      "properties": {
                        "pending_approval_count": {
                          "type": "integer",
                          "description": "Submissions awaiting the caller's review. Role-aware, matching the size of GET /api/v1/forms/approvals so the badge can't drift from that list: admins and Forms App Admins count the business-wide pending_review set (submitted + under_review); a manager counts only the under_review submissions from their own direct reports.\n"
                        }
                      }
                    },
                    "notifications": {
                      "type": "object",
                      "properties": {
                        "unread_count": {
                          "type": "integer",
                          "description": "Number of unread notifications"
                        },
                        "total_count": {
                          "type": "integer",
                          "description": "Total notifications for user"
                        },
                        "recent": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "title": {
                                "type": "string"
                              },
                              "message": {
                                "type": "string"
                              },
                              "type": {
                                "type": "string"
                              },
                              "priority": {
                                "type": "string",
                                "enum": [
                                  "low",
                                  "normal",
                                  "high",
                                  "urgent"
                                ],
                                "default": "normal"
                              },
                              "created_at": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "read_at": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true
                              },
                              "action_url": {
                                "type": "string",
                                "nullable": true
                              }
                            }
                          }
                        }
                      }
                    },
                    "quick_stats": {
                      "type": "object",
                      "properties": {
                        "today_shifts": {
                          "type": "integer",
                          "description": "Number of shifts today"
                        },
                        "week_hours": {
                          "type": "number",
                          "format": "float",
                          "description": "Hours worked this week"
                        },
                        "unread_notifications": {
                          "type": "integer",
                          "description": "Unread notification count"
                        },
                        "pending_approvals": {
                          "type": "integer",
                          "description": "Items pending approval (managers/admins only)"
                        },
                        "team_size": {
                          "type": "integer",
                          "description": "Team size (managers/admins only)"
                        },
                        "today_employee_count": {
                          "type": "integer",
                          "description": "Employees scheduled today (managers/admins only)"
                        }
                      }
                    },
                    "_meta": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/PiggybackMeta"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "refresh_interval": {
                              "type": "integer",
                              "description": "Recommended refresh interval in seconds",
                              "example": 300
                            },
                            "server_time": {
                              "type": "string",
                              "format": "date-time",
                              "description": "Current server time"
                            },
                            "mobile_optimized": {
                              "type": "boolean",
                              "example": true
                            },
                            "cache_key": {
                              "type": "string",
                              "description": "Cache key for client-side caching"
                            }
                          }
                        }
                      ]
                    }
                  }
                },
                "examples": {
                  "employee_home": {
                    "summary": "Employee home screen",
                    "value": {
                      "user": {
                        "id": 123,
                        "email": "john.doe@company.com",
                        "first_name": "John",
                        "last_name": "Doe",
                        "full_name": "John Doe",
                        "role": "employee",
                        "active": true,
                        "avatar_url": null,
                        "phone": "+1234567890",
                        "onboarding_completed": true,
                        "onboarding_progress": 100,
                        "preferences": {}
                      },
                      "business": {
                        "id": 456,
                        "name": "Example Company",
                        "subdomain": "example",
                        "timezone": "America/New_York",
                        "logo_url": null,
                        "branding": {
                          "primary_button_color": "#2e63b3",
                          "secondary_button_color": "#6c757d",
                          "status_success_color": "#198754",
                          "status_error_color": "#dc3545",
                          "status_warning_color": "#ffc107",
                          "status_info_color": "#0dcaf0",
                          "mobile_header_background": "#2e63b3",
                          "mobile_header_text": "#FFFFFF",
                          "mobile_footer_background": "#FFFFFF",
                          "mobile_footer_icon_active": "#2e63b3",
                          "mobile_footer_icon_inactive": "#6c757d"
                        },
                        "settings": {
                          "shifts_scheduling_enabled": true,
                          "time_attendance_enabled": true,
                          "leave_management_enabled": true,
                          "enabled_marketplace_apps": [
                            "epms",
                            "training_connect"
                          ],
                          "week_start_day": "monday"
                        }
                      },
                      "dashboard": {
                        "widgets": [
                          {
                            "name": "quick_actions",
                            "title": "Quick Actions",
                            "category": "platform",
                            "icon": "fas fa-bolt",
                            "size": "col-12",
                            "priority": 1,
                            "data": {
                              "actions": [
                                {
                                  "title": "Clock In",
                                  "path": "/attendance/clock_in",
                                  "icon": "fas fa-play",
                                  "variant": "primary"
                                },
                                {
                                  "title": "My Schedule",
                                  "path": "/shifts/my_shifts",
                                  "icon": "fas fa-calendar",
                                  "variant": "outline-info"
                                }
                              ]
                            },
                            "data_schema": {
                              "type": "object",
                              "properties": {
                                "actions": {
                                  "type": "array",
                                  "items": {
                                    "type": "object",
                                    "properties": {
                                      "title": {
                                        "type": "string"
                                      },
                                      "path": {
                                        "type": "string"
                                      },
                                      "icon": {
                                        "type": "string"
                                      },
                                      "variant": {
                                        "type": "string"
                                      }
                                    }
                                  }
                                }
                              }
                            },
                            "version": "1.2",
                            "refresh_interval": 300
                          },
                          {
                            "name": "upcoming_shifts",
                            "title": "Upcoming Shifts",
                            "category": "core_app",
                            "icon": "fas fa-calendar",
                            "size": "col-md-6",
                            "priority": 3,
                            "data": {
                              "shifts": [
                                {
                                  "id": 789,
                                  "title": "Morning Shift",
                                  "start_time": "2024-01-15T08:00:00Z",
                                  "end_time": "2024-01-15T16:00:00Z",
                                  "location": "Main Office"
                                }
                              ],
                              "total_count": 5
                            },
                            "refresh_interval": 300
                          },
                          {
                            "name": "quick_attendance",
                            "title": "Time Clock",
                            "category": "core_app",
                            "icon": "fas fa-clock",
                            "size": "col-md-6",
                            "priority": 2,
                            "data": {
                              "current_status": {
                                "status": "scheduled",
                                "shift": {
                                  "id": 789,
                                  "date": "2024-01-15",
                                  "start_time": "08:00 AM",
                                  "end_time": "04:00 PM",
                                  "location": "Main Office",
                                  "duration": "8h 0m",
                                  "is_ad_hoc": false
                                },
                                "can_clock_in": true
                              },
                              "today_hours": 0.0,
                              "markdown_summary": "Next Shift: Today, 08:00 AM - 04:00 PM at Main Office",
                              "display_hint": "card"
                            },
                            "refresh_interval": 60
                          }
                        ],
                        "widget_count": 3,
                        "performance_metrics": {
                          "current_week": {
                            "hours_worked": 32.5,
                            "shifts_completed": 4,
                            "on_time_rate": 95.2,
                            "attendance_rate": 98.1
                          },
                          "current_month": {
                            "total_shifts": 18,
                            "hours_worked": 142.5,
                            "performance_score": 96.5
                          }
                        },
                        "role_context": {
                          "current_role": "employee",
                          "permissions": [
                            "read_dashboard",
                            "view_profile"
                          ],
                          "available_actions": [
                            {
                              "name": "view_profile",
                              "label": "View Profile",
                              "icon": "fas fa-user"
                            },
                            {
                              "name": "clock_in_out",
                              "label": "Clock In/Out",
                              "icon": "fas fa-clock"
                            }
                          ]
                        }
                      },
                      "notifications": {
                        "unread_count": 3,
                        "total_count": 25,
                        "recent": [
                          {
                            "id": 101,
                            "title": "Schedule Update",
                            "message": "Your shift on Jan 15 has been updated",
                            "type": "shift_update",
                            "priority": "normal",
                            "created_at": "2024-01-14T10:30:00Z",
                            "read_at": null,
                            "action_url": "/shifts/789"
                          }
                        ]
                      },
                      "quick_stats": {
                        "today_shifts": 1,
                        "week_hours": 32.5,
                        "unread_notifications": 3
                      },
                      "_meta": {
                        "api_version": "v1",
                        "generated_at": "2024-01-15T12:00:00Z",
                        "refresh_interval": 300,
                        "server_time": "2024-01-15T12:00:00Z",
                        "mobile_optimized": true,
                        "cache_key": "home_123_456_1705320000"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "description": "Internal server error",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "home_data_error"
                        },
                        "message": {
                          "type": "string",
                          "example": "Failed to load home screen data"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/users/me": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Get current user profile (Enhanced)",
        "description": "Returns the authenticated user's profile and business information with optional\npiggyback data for enhanced mobile and web app experiences.\n\n## Enhanced Features\nUse the `include` query parameter to request additional data:\n\n- `complete_profile` - Full profile with professional and compensation data\n- `dashboard_summary` - Dashboard stats and quick actions\n- `intelligence_insights` - AI-powered insights and recommendations\n- `preferences_detailed` - Detailed preferences with metadata\n\n## Examples\n```\nGET /api/v1/users/me?include=complete_profile,dashboard_summary\nGET /api/v1/users/me?include=intelligence_insights\n```\n",
        "parameters": [
          {
            "name": "include",
            "in": "query",
            "description": "Comma-separated list of additional data to include in the response.\nAvailable options: complete_profile, dashboard_summary, intelligence_insights, preferences_detailed\n",
            "required": false,
            "schema": {
              "type": "string",
              "example": "complete_profile,dashboard_summary"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "User profile retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/User"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "permissions": {
                              "type": "array",
                              "items": {
                                "type": "string"
                              },
                              "description": "User's permissions"
                            },
                            "feature_flags": {
                              "type": "object",
                              "description": "Feature flags for the user"
                            }
                          }
                        }
                      ]
                    },
                    "business": {
                      "$ref": "#/components/schemas/Business"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/PiggybackMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/users/me/preferences": {
      "patch": {
        "tags": [
          "Users"
        ],
        "summary": "Update user preferences (Enhanced)",
        "description": "Update the authenticated user's preferences with support for category-based updates\nand smart defaults integration.\n\n## Enhanced Features\n- Category-specific updates using `?category=notifications`\n- Smart defaults with `?include_smart_defaults=true`\n- Atomic updates to prevent conflicts\n\n## Examples\n```\nPATCH /api/v1/users/me/preferences?category=notifications\nPATCH /api/v1/users/me/preferences?include_smart_defaults=true\n```\n",
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "description": "Update only a specific preference category.\nAvailable categories: notifications, availability, communication, privacy\n",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "notifications",
                "availability",
                "communication",
                "privacy"
              ],
              "example": "notifications"
            }
          },
          {
            "name": "include_smart_defaults",
            "in": "query",
            "description": "Include AI-powered smart defaults in the response",
            "required": false,
            "schema": {
              "type": "boolean",
              "example": true
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "preferences"
                ],
                "properties": {
                  "preferences": {
                    "type": "object",
                    "description": "User preferences to update",
                    "example": {
                      "theme": "dark",
                      "notifications_enabled": true,
                      "language": "en"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Preferences updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "preferences": {
                          "type": "object"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/users/{id}/profile": {
      "get": {
        "tags": [
          "Users"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Mobile profile card for a user (numeric id lookup)",
        "description": "Returns a mobile-friendly profile card for the user with the given\nnumeric `User#id` — the value mobile clients receive from feed\npayloads, comment author blocks, and mention search results.\n\nDistinct from `GET /api/v1/users/{id}` which is the HRIS endpoint\nkeyed on `mango_employee_id`. Tenant-scoped: only users with an\nactive `UserBusiness` row in the caller's business are resolvable;\ncross-tenant or inactive ids return 404.\n\nDirect reports are filtered to the caller's business + active\nmemberships, so the list never leaks ex-employees or users from\nother tenants the manager may belong to.\n\n`profile_picture.*` URLs and `mobile_url` are absolute so JSON\nclients can render / open them directly without resolving against\na base URL.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "description": "Numeric User#id."
          }
        ],
        "responses": {
          "200": {
            "description": "Mobile profile card",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "user"
                  ],
                  "properties": {
                    "user": {
                      "type": "object",
                      "required": [
                        "id",
                        "name",
                        "email",
                        "profile_picture",
                        "direct_reports",
                        "mobile_url"
                      ],
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string",
                          "nullable": true
                        },
                        "email": {
                          "type": "string",
                          "format": "email",
                          "nullable": true
                        },
                        "contact": {
                          "type": "string",
                          "nullable": true,
                          "description": "Primary phone number (User#phone)."
                        },
                        "employee_id": {
                          "type": "string",
                          "nullable": true,
                          "description": "HRIS-facing mango_employee_id (e.g. E1234567)."
                        },
                        "job_title": {
                          "type": "string",
                          "nullable": true
                        },
                        "employment_type": {
                          "type": "string",
                          "nullable": true,
                          "description": "One of EMPLOYEE_TYPES: Full-Time, Part-Time, Seasonal/Temp, Hourly, Salaried, Contractor, Other."
                        },
                        "hire_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "profile_picture": {
                          "type": "object",
                          "properties": {
                            "thumbnail_url": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true,
                              "description": "40x40 variant; absolute URL."
                            },
                            "full_url": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true,
                              "description": "200x200 variant; absolute URL."
                            }
                          }
                        },
                        "primary_location": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "address": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "direct_reports": {
                          "type": "array",
                          "description": "Direct reports filtered to the caller's business + active UserBusiness.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string",
                                "nullable": true
                              },
                              "email": {
                                "type": "string",
                                "format": "email",
                                "nullable": true
                              },
                              "profile_picture": {
                                "type": "object",
                                "properties": {
                                  "thumbnail_url": {
                                    "type": "string",
                                    "format": "uri",
                                    "nullable": true
                                  },
                                  "full_url": {
                                    "type": "string",
                                    "format": "uri",
                                    "nullable": true
                                  }
                                }
                              }
                            }
                          }
                        },
                        "mobile_url": {
                          "type": "string",
                          "format": "uri",
                          "description": "Absolute deep link to the user's mobile profile,\nfollowing the `/m/...` convention used by\n`mobile_app_url(slug)`. Clients can hand this to\nthe WebView or in-app router.\n"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "id is not numeric"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "User not found or not in the caller business"
          }
        }
      }
    },
    "/profile": {
      "get": {
        "tags": [
          "User Profile Management"
        ],
        "summary": "Get complete user profile (redirects to /users/me)",
        "description": "Redirects to the enhanced /users/me endpoint. This endpoint is provided \nfor consistency but clients should use /users/me directly with include parameters.\n",
        "responses": {
          "302": {
            "description": "Redirect to /users/me"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/profile/actions": {
      "post": {
        "tags": [
          "User Profile Management"
        ],
        "summary": "Execute profile actions",
        "description": "Execute various profile-related actions in a single endpoint. This allows\nfor specific operations like avatar updates, skill verification requests,\ncareer goal setting, and more.\n\n**Note:** Use `action_type` parameter (preferred) or `action` parameter for the action. \nUsing `action_type` is recommended as `action` is a reserved Rails parameter.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "action_type"
                ],
                "properties": {
                  "action_type": {
                    "type": "string",
                    "enum": [
                      "update_avatar",
                      "request_skill_verification",
                      "set_career_goal",
                      "update_emergency_contact",
                      "request_manager_change",
                      "update_communication_preferences"
                    ],
                    "description": "The action to perform (preferred parameter)",
                    "example": "update_avatar"
                  },
                  "action": {
                    "type": "string",
                    "enum": [
                      "update_avatar",
                      "request_skill_verification",
                      "set_career_goal",
                      "update_emergency_contact",
                      "request_manager_change",
                      "update_communication_preferences"
                    ],
                    "description": "The action to perform (legacy, use action_type instead)",
                    "deprecated": true,
                    "example": "update_avatar"
                  },
                  "data": {
                    "type": "object",
                    "description": "Action-specific data",
                    "example": {
                      "avatar": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD..."
                    }
                  }
                }
              },
              "examples": {
                "update_avatar": {
                  "summary": "Update profile avatar",
                  "value": {
                    "action_type": "update_avatar",
                    "data": {
                      "avatar": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD..."
                    }
                  }
                },
                "request_skill_verification": {
                  "summary": "Request skill verification",
                  "value": {
                    "action_type": "request_skill_verification",
                    "data": {
                      "skill_id": 123
                    }
                  }
                },
                "set_career_goal": {
                  "summary": "Set career goal",
                  "value": {
                    "action_type": "set_career_goal",
                    "data": {
                      "goal": "Complete React Certification",
                      "target_date": "2024-06-01"
                    }
                  }
                },
                "update_emergency_contact": {
                  "summary": "Update emergency contact",
                  "value": {
                    "action_type": "update_emergency_contact",
                    "data": {
                      "contact_info": {
                        "name": "Jane Doe",
                        "phone": "+1234567890",
                        "relationship": "Spouse"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Action executed successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Avatar updated successfully"
                    },
                    "data": {
                      "type": "object",
                      "description": "Action-specific response data"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/profile/bulk": {
      "patch": {
        "tags": [
          "User Profile Management"
        ],
        "summary": "Bulk update profile sections",
        "description": "Update multiple profile sections atomically in a single transaction.\nThis ensures data consistency when updating related profile information.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "updates"
                ],
                "properties": {
                  "updates": {
                    "type": "object",
                    "description": "Profile sections to update",
                    "properties": {
                      "personal": {
                        "type": "object",
                        "description": "Personal information updates",
                        "properties": {
                          "first_name": {
                            "type": "string",
                            "example": "John"
                          },
                          "last_name": {
                            "type": "string",
                            "example": "Doe"
                          },
                          "phone": {
                            "type": "string",
                            "example": "+1234567890"
                          },
                          "time_zone": {
                            "type": "string",
                            "example": "America/New_York"
                          },
                          "employee_type": {
                            "type": "string",
                            "example": "Full-Time"
                          },
                          "job_title": {
                            "type": "string",
                            "example": "Senior Developer"
                          },
                          "department": {
                            "type": "string",
                            "example": "Engineering"
                          }
                        }
                      },
                      "preferences": {
                        "type": "object",
                        "description": "Preferences updates by category",
                        "properties": {
                          "notifications": {
                            "type": "object",
                            "example": {
                              "shift_reminders": true,
                              "schedule_changes": true
                            }
                          },
                          "availability": {
                            "type": "object",
                            "example": {
                              "willing_for_extra_shifts": true,
                              "max_shifts_per_week": 5
                            }
                          },
                          "communication": {
                            "type": "object",
                            "example": {
                              "preferred_method": "email",
                              "frequency": "immediate"
                            }
                          }
                        }
                      },
                      "professional": {
                        "type": "object",
                        "description": "Professional information updates",
                        "properties": {
                          "skills_for_roles": {
                            "type": "object",
                            "example": {
                              "developer": [
                                "JavaScript",
                                "React",
                                "Node.js"
                              ]
                            }
                          }
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "updates": {
                  "personal": {
                    "phone": "+1234567890",
                    "time_zone": "America/New_York"
                  },
                  "preferences": {
                    "notifications": {
                      "shift_reminders": true,
                      "schedule_changes": true
                    },
                    "availability": {
                      "willing_for_extra_shifts": true,
                      "max_shifts_per_week": 5
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Bulk update completed successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Bulk update completed successfully"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "updated_sections": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "example": [
                            "personal",
                            "preferences"
                          ]
                        },
                        "results": {
                          "type": "object",
                          "description": "Results for each updated section"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/users/me/security": {
      "get": {
        "tags": [
          "Account Settings"
        ],
        "summary": "Get user security settings",
        "description": "Retrieve comprehensive security settings for the authenticated user including\ntwo-factor authentication status, trusted devices, security questions, and login history.\n",
        "responses": {
          "200": {
            "description": "Security settings retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "security": {
                      "$ref": "#/components/schemas/UserSecuritySettings"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "patch": {
        "tags": [
          "Account Settings"
        ],
        "summary": "Update user security settings",
        "description": "Update security settings including two-factor authentication, trusted devices,\nand security questions.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "security": {
                    "type": "object",
                    "properties": {
                      "two_factor_auth": {
                        "type": "object",
                        "properties": {
                          "enabled": {
                            "type": "boolean",
                            "description": "Enable or disable two-factor authentication"
                          }
                        }
                      },
                      "trusted_devices": {
                        "type": "object",
                        "properties": {
                          "remove_device_id": {
                            "type": "string",
                            "description": "ID of device to remove from trusted list"
                          }
                        }
                      },
                      "security_questions": {
                        "type": "object",
                        "properties": {
                          "question_1": {
                            "type": "string",
                            "description": "First security question"
                          },
                          "answer_1": {
                            "type": "string",
                            "description": "Answer to first security question"
                          },
                          "question_2": {
                            "type": "string",
                            "description": "Second security question (optional)"
                          },
                          "answer_2": {
                            "type": "string",
                            "description": "Answer to second security question (optional)"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Security settings updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "security": {
                      "$ref": "#/components/schemas/UserSecuritySettings"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/account_settings/dashboard": {
      "get": {
        "tags": [
          "Account Settings"
        ],
        "summary": "Get account settings dashboard",
        "description": "Retrieve a comprehensive dashboard view of account settings including\nprofile completion status, security overview, notification summary, and quick actions.\n",
        "responses": {
          "200": {
            "description": "Dashboard data retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "dashboard": {
                      "$ref": "#/components/schemas/AccountSettingsDashboard"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/account_settings/privacy": {
      "get": {
        "tags": [
          "Account Settings"
        ],
        "summary": "Get privacy settings",
        "description": "Retrieve privacy and data management settings including data sharing preferences,\nvisibility settings, and data retention options.\n",
        "responses": {
          "200": {
            "description": "Privacy settings retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "privacy": {
                      "$ref": "#/components/schemas/UserPrivacySettings"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "patch": {
        "tags": [
          "Account Settings"
        ],
        "summary": "Update privacy settings",
        "description": "Update privacy and data management settings including data sharing,\nvisibility preferences, and data retention options.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "privacy": {
                    "$ref": "#/components/schemas/UserPrivacySettingsUpdate"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Privacy settings updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "privacy": {
                      "$ref": "#/components/schemas/UserPrivacySettings"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/account_settings/communication": {
      "get": {
        "tags": [
          "Account Settings"
        ],
        "summary": "Get communication preferences",
        "description": "Retrieve communication preferences including language, timezone,\ncommunication style, AI assistant settings, and meeting preferences.\n",
        "responses": {
          "200": {
            "description": "Communication settings retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "communication": {
                      "$ref": "#/components/schemas/UserCommunicationSettings"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "patch": {
        "tags": [
          "Account Settings"
        ],
        "summary": "Update communication preferences",
        "description": "Update communication preferences including language, timezone,\ncommunication style, AI assistant settings, and meeting preferences.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "communication": {
                    "$ref": "#/components/schemas/UserCommunicationSettingsUpdate"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Communication settings updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "communication": {
                      "$ref": "#/components/schemas/UserCommunicationSettings"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/onboarding": {
      "get": {
        "tags": [
          "Onboarding"
        ],
        "summary": "Get onboarding progress",
        "description": "Returns the current user's onboarding status, progress percentage, and next steps.\nThis endpoint provides a complete overview of the onboarding journey for mobile apps.\n",
        "responses": {
          "200": {
            "description": "Onboarding progress retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "completed": {
                      "type": "boolean",
                      "description": "Whether onboarding is fully completed",
                      "example": false
                    },
                    "current_step": {
                      "type": "string",
                      "description": "Current onboarding step name",
                      "example": "availability_shift_preferences"
                    },
                    "progress_percentage": {
                      "type": "integer",
                      "description": "Completion percentage (0-100)",
                      "example": 60
                    },
                    "steps_completed": {
                      "type": "integer",
                      "description": "Number of steps completed",
                      "example": 3
                    },
                    "steps_total": {
                      "type": "integer",
                      "description": "Total number of onboarding steps",
                      "example": 6
                    },
                    "estimated_completion_time": {
                      "type": "string",
                      "description": "Estimated time to complete remaining steps",
                      "example": "10-15 minutes"
                    },
                    "next_action": {
                      "type": "string",
                      "nullable": true,
                      "description": "URL for the next step to complete",
                      "example": "/api/v1/onboarding/steps/availability_shift_preferences"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/onboarding/steps": {
      "get": {
        "tags": [
          "Onboarding"
        ],
        "summary": "List all onboarding steps",
        "description": "Returns a complete list of onboarding steps with their status, accessibility, \nand metadata. Useful for displaying progress indicators and step navigation.\n",
        "responses": {
          "200": {
            "description": "Onboarding steps retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "steps": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string",
                            "description": "Step identifier",
                            "example": "profile_completion"
                          },
                          "display_name": {
                            "type": "string",
                            "description": "Human-readable step name",
                            "example": "Complete Your Profile"
                          },
                          "position": {
                            "type": "integer",
                            "description": "Step position in sequence",
                            "example": 1
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "pending",
                              "completed",
                              "skipped"
                            ],
                            "description": "Current step status",
                            "example": "completed"
                          },
                          "completed_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "description": "When step was completed"
                          },
                          "accessible": {
                            "type": "boolean",
                            "description": "Whether step can be accessed now",
                            "example": true
                          },
                          "required": {
                            "type": "boolean",
                            "description": "Whether step is required",
                            "example": true
                          },
                          "estimated_time": {
                            "type": "string",
                            "description": "Estimated completion time",
                            "example": "3-5 minutes"
                          },
                          "description": {
                            "type": "string",
                            "description": "Step description",
                            "example": "Complete your basic profile information"
                          },
                          "url": {
                            "type": "string",
                            "description": "API URL for this step",
                            "example": "/api/v1/onboarding/steps/profile_completion"
                          }
                        }
                      }
                    },
                    "total_count": {
                      "type": "integer",
                      "description": "Total number of steps",
                      "example": 6
                    },
                    "completed_count": {
                      "type": "integer",
                      "description": "Number of completed steps",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/onboarding/steps/{step_name}": {
      "get": {
        "tags": [
          "Onboarding"
        ],
        "summary": "Get step details",
        "description": "Returns detailed information about a specific onboarding step including\nform fields, current data, validation rules, and help text.\n",
        "parameters": [
          {
            "name": "step_name",
            "in": "path",
            "required": true,
            "description": "Name of the onboarding step",
            "schema": {
              "type": "string",
              "enum": [
                "profile_completion",
                "role_location_assignment",
                "availability_shift_preferences",
                "secondary_location_preferences",
                "notification_preferences",
                "security_setup"
              ],
              "example": "profile_completion"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Step details retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string",
                      "example": "profile_completion"
                    },
                    "display_name": {
                      "type": "string",
                      "example": "Complete Your Profile"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "completed",
                        "skipped"
                      ],
                      "example": "pending"
                    },
                    "completed_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "accessible": {
                      "type": "boolean",
                      "example": true
                    },
                    "required": {
                      "type": "boolean",
                      "example": true
                    },
                    "estimated_time": {
                      "type": "string",
                      "example": "3-5 minutes"
                    },
                    "description": {
                      "type": "string",
                      "example": "Complete your basic profile information"
                    },
                    "form_fields": {
                      "type": "array",
                      "description": "Form fields for this step",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string",
                            "example": "first_name"
                          },
                          "type": {
                            "type": "string",
                            "example": "text"
                          },
                          "required": {
                            "type": "boolean",
                            "example": true
                          },
                          "label": {
                            "type": "string",
                            "example": "First Name"
                          }
                        }
                      }
                    },
                    "current_data": {
                      "type": "object",
                      "description": "Current values for form fields",
                      "example": {
                        "first_name": "John",
                        "last_name": "Doe",
                        "email": "john.doe@example.com"
                      }
                    },
                    "validation_rules": {
                      "type": "object",
                      "description": "Validation rules for form fields",
                      "example": {
                        "first_name": {
                          "required": true,
                          "min_length": 1
                        }
                      }
                    },
                    "help_text": {
                      "type": "string",
                      "nullable": true,
                      "description": "Help text for this step",
                      "example": "This information helps us personalize your experience"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Step not accessible",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Onboarding"
        ],
        "summary": "Complete onboarding step",
        "description": "Updates and completes a specific onboarding step with the provided data.\nThis will validate the input, save the data, and update the user's progress.\n",
        "parameters": [
          {
            "name": "step_name",
            "in": "path",
            "required": true,
            "description": "Name of the onboarding step",
            "schema": {
              "type": "string",
              "enum": [
                "profile_completion",
                "role_location_assignment",
                "availability_shift_preferences",
                "secondary_location_preferences",
                "notification_preferences",
                "security_setup"
              ],
              "example": "profile_completion"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Step-specific data (varies by step)",
                "example": {
                  "user": {
                    "first_name": "John",
                    "last_name": "Doe",
                    "email": "john.doe@example.com",
                    "phone": "+1234567890",
                    "time_zone": "America/New_York"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Step completed successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "step_completed": {
                      "type": "boolean",
                      "example": true
                    },
                    "current_step": {
                      "type": "string",
                      "nullable": true,
                      "description": "Next step to complete",
                      "example": "role_location_assignment"
                    },
                    "progress_percentage": {
                      "type": "integer",
                      "example": 33
                    },
                    "onboarding_completed": {
                      "type": "boolean",
                      "example": false
                    },
                    "next_step": {
                      "type": "string",
                      "nullable": true,
                      "example": "role_location_assignment"
                    },
                    "next_action": {
                      "type": "string",
                      "nullable": true,
                      "example": "/api/v1/onboarding/steps/role_location_assignment"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Step not accessible",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/onboarding/steps/{step_name}/enhanced": {
      "get": {
        "tags": [
          "Onboarding"
        ],
        "summary": "Get enhanced step information with AI assistance",
        "description": "Returns comprehensive step information including smart defaults, AI suggestions,\nand intelligent assistance. This endpoint consolidates multiple features:\n- Smart form pre-filling based on user context\n- AI-powered preference suggestions\n- Similar employee patterns and benchmarks\n- Contextual tips and completion guidance\n- Personalized time estimates\n",
        "parameters": [
          {
            "name": "step_name",
            "in": "path",
            "required": true,
            "description": "Name of the onboarding step",
            "schema": {
              "type": "string",
              "enum": [
                "profile_completion",
                "role_location_assignment",
                "availability_shift_preferences",
                "secondary_location_preferences",
                "notification_preferences",
                "security_setup"
              ],
              "example": "profile_completion"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Enhanced step information retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string",
                      "example": "profile_completion"
                    },
                    "display_name": {
                      "type": "string",
                      "example": "Complete Your Profile"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "completed",
                        "skipped"
                      ],
                      "example": "pending"
                    },
                    "completed_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "accessible": {
                      "type": "boolean",
                      "example": true
                    },
                    "required": {
                      "type": "boolean",
                      "example": true
                    },
                    "estimated_time": {
                      "type": "string",
                      "example": "3-5 minutes"
                    },
                    "description": {
                      "type": "string",
                      "example": "Complete your basic profile information"
                    },
                    "form_fields": {
                      "type": "array",
                      "description": "Enhanced form fields with smart suggestions",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string",
                            "example": "time_zone"
                          },
                          "type": {
                            "type": "string",
                            "example": "select"
                          },
                          "required": {
                            "type": "boolean",
                            "example": true
                          },
                          "label": {
                            "type": "string",
                            "example": "Time Zone"
                          },
                          "suggested_value": {
                            "type": "string",
                            "nullable": true,
                            "example": "Pacific Time (US & Canada)"
                          },
                          "confidence": {
                            "type": "string",
                            "enum": [
                              "high",
                              "medium",
                              "low"
                            ],
                            "example": "high"
                          },
                          "smart_help": {
                            "type": "string",
                            "nullable": true,
                            "example": "We detected your timezone automatically"
                          }
                        }
                      }
                    },
                    "current_data": {
                      "type": "object",
                      "description": "Current values with smart defaults applied",
                      "example": {
                        "first_name": "John",
                        "last_name": "Doe",
                        "time_zone": "Pacific Time (US & Canada)"
                      }
                    },
                    "ai_suggestions": {
                      "type": "object",
                      "description": "AI-powered suggestions and insights",
                      "properties": {
                        "timezone_confidence": {
                          "type": "string",
                          "example": "high"
                        },
                        "profile_completeness_score": {
                          "type": "integer",
                          "example": 75
                        },
                        "suggested_improvements": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "example": [
                            "Add a profile photo to help colleagues recognize you"
                          ]
                        }
                      }
                    },
                    "smart_defaults": {
                      "type": "object",
                      "description": "Intelligent defaults based on context",
                      "properties": {
                        "timezone_detected": {
                          "type": "string",
                          "nullable": true,
                          "example": "Pacific Time (US & Canada)"
                        },
                        "role_based_suggestions": {
                          "type": "object",
                          "example": {
                            "role_name": "Software Engineer",
                            "typical_hours": {
                              "min": 35,
                              "max": 40
                            }
                          }
                        }
                      }
                    },
                    "similar_employee_patterns": {
                      "type": "object",
                      "description": "Patterns from similar employees",
                      "properties": {
                        "common_availability_patterns": {
                          "type": "object",
                          "example": {
                            "avg_min_hours": 20,
                            "avg_max_hours": 40
                          }
                        },
                        "typical_completion_time": {
                          "type": "string",
                          "example": "2 days"
                        },
                        "success_factors": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "example": [
                            "Complete profiles (85% average)",
                            "Email notifications enabled (78%)"
                          ]
                        }
                      }
                    },
                    "completion_tips": {
                      "type": "array",
                      "description": "Contextual tips for completing this step",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "Adding a profile photo helps colleagues recognize you",
                        "Setting your correct timezone ensures accurate scheduling"
                      ]
                    },
                    "common_mistakes": {
                      "type": "array",
                      "description": "Common mistakes to avoid",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "Forgetting to set timezone (causes scheduling issues)",
                        "Using incomplete contact information"
                      ]
                    },
                    "estimated_completion_time": {
                      "type": "string",
                      "description": "Personalized time estimate",
                      "example": "3-5 minutes"
                    },
                    "progress_context": {
                      "type": "object",
                      "description": "Progress context and benchmarks",
                      "properties": {
                        "steps_remaining": {
                          "type": "integer",
                          "example": 3
                        },
                        "estimated_total_time": {
                          "type": "string",
                          "example": "12 minutes"
                        },
                        "completion_rate_similar_users": {
                          "type": "integer",
                          "description": "Completion rate percentage for similar users",
                          "example": 87
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Step not accessible",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/onboarding/steps/{step_name}/validate": {
      "post": {
        "tags": [
          "Onboarding"
        ],
        "summary": "Validate step data",
        "description": "Validates onboarding step data without saving it. Useful for real-time\nform validation in mobile apps before final submission.\n",
        "parameters": [
          {
            "name": "step_name",
            "in": "path",
            "required": true,
            "description": "Name of the onboarding step",
            "schema": {
              "type": "string",
              "enum": [
                "profile_completion",
                "role_location_assignment",
                "availability_shift_preferences",
                "secondary_location_preferences",
                "notification_preferences",
                "security_setup"
              ],
              "example": "profile_completion"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Step-specific data to validate",
                "example": {
                  "user": {
                    "first_name": "John",
                    "last_name": "Doe",
                    "email": "john.doe@example.com"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Validation successful",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "valid": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Validation successful"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "description": "Validation failed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "valid": {
                      "type": "boolean",
                      "example": false
                    },
                    "errors": {
                      "type": "object",
                      "description": "Field-specific validation errors",
                      "example": {
                        "first_name": [
                          "First name is required"
                        ],
                        "email": [
                          "Please enter a valid email address"
                        ]
                      }
                    },
                    "warnings": {
                      "type": "array",
                      "description": "Non-blocking validation warnings",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "Consider adding a phone number for better communication"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/onboarding/reset": {
      "post": {
        "tags": [
          "Onboarding"
        ],
        "summary": "Reset onboarding progress",
        "description": "Resets the user's onboarding progress back to the beginning. This will\nclear all completed steps and preferences. Requires confirmation.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "confirm"
                ],
                "properties": {
                  "confirm": {
                    "type": "string",
                    "description": "Must be \"true\" to confirm reset",
                    "example": "true"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Onboarding reset successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "reset": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Onboarding has been reset successfully"
                    },
                    "current_step": {
                      "type": "string",
                      "example": "profile_completion"
                    },
                    "next_action": {
                      "type": "string",
                      "example": "/api/v1/onboarding/steps/profile_completion"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/forms": {
      "get": {
        "tags": [
          "Forms"
        ],
        "summary": "List published forms (Published Forms page)",
        "description": "Returns all published forms for the business, matching exactly what appears\non the web **Published Forms** page (`/apps/forms/published`).\n\n- Ordered alphabetically by name (A→Z)\n- Excludes survey-category templates and forms linked to a Survey\n- No per-user permission filtering — all authenticated users see all published forms\n- Each item includes `page_count` (number of pages in the form)\n\n**📋 Complete Forms API**: For full Forms App API documentation including\ntemplate management, public sharing, and integration endpoints, see the\ndedicated Forms App schema at `/api-docs/forms-app-schema.yaml`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Case-insensitive substring match on form name",
            "schema": {
              "type": "string",
              "example": "incident report"
            }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Filter by form category slug",
            "schema": {
              "type": "string",
              "example": "safety"
            }
          },
          {
            "name": "priority",
            "in": "query",
            "required": false,
            "description": "Filter by form priority",
            "schema": {
              "type": "string",
              "enum": [
                "urgent",
                "high",
                "normal",
                "low"
              ],
              "example": "high"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number for pagination (default 1)",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Items per page, 1–50 (default 20)",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Published forms retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FormSummaryItem"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/forms/my_submissions": {
      "get": {
        "tags": [
          "Forms"
        ],
        "summary": "List current user's form submissions (My Submissions tab)",
        "description": "Returns the authenticated user's own form submissions, matching the web\n**My Submissions** page (`/apps/forms/my_submissions`).\n\n- Ordered by `updated_at DESC` — most recently touched first\n- Scope: only submissions belonging to the current user and business\n- The `status` parameter maps to the 6 tabs in the mobile UI:\n\n| `status` value | Mobile tab |\n|---|---|\n| _(omit)_ or `all` | All Statuses |\n| `draft` | Saved Drafts |\n| `submitted` | Submitted |\n| `under_review` | Under Review |\n| `approved` | Approved |\n| `rejected` | Rejected |\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Status tab filter. Omit or pass `all` for every status.\nAllowed values: all, draft, submitted, under_review, approved, rejected.\n",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "draft",
                "submitted",
                "under_review",
                "approved",
                "rejected"
              ],
              "example": "draft"
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Case-insensitive substring match on form name",
            "schema": {
              "type": "string",
              "example": "safety audit"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number (default 1)",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Items per page, 1–50 (default 20)",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Submissions retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MySubmissionItem"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PaginationMeta"
                    },
                    "segment_counts": {
                      "type": "object",
                      "description": "Per-status tab badge counts. Present ONLY on the all view (status=all or omitted); absent when a specific status is requested. Counts honor the active `search` filter. `all` is the grand total across every status and may exceed the sum of the five buckets when submissions sit in a status without its own tab (e.g. pending_completion).",
                      "properties": {
                        "all": {
                          "type": "integer",
                          "example": 7
                        },
                        "draft": {
                          "type": "integer",
                          "example": 1
                        },
                        "submitted": {
                          "type": "integer",
                          "example": 2
                        },
                        "under_review": {
                          "type": "integer",
                          "example": 1
                        },
                        "approved": {
                          "type": "integer",
                          "example": 2
                        },
                        "rejected": {
                          "type": "integer",
                          "example": 1
                        }
                      },
                      "required": [
                        "all",
                        "draft",
                        "submitted",
                        "under_review",
                        "approved",
                        "rejected"
                      ]
                    }
                  }
                },
                "examples": {
                  "all_statuses": {
                    "summary": "All statuses (default)",
                    "value": {
                      "items": [
                        {
                          "id": 1042,
                          "form_id": 7,
                          "form_name": "Equipment Transfer",
                          "form_description": "Transfer equipment custody between sites",
                          "form_category": "operations",
                          "status": "draft",
                          "status_label": "Saved draft",
                          "completion_percentage": 85,
                          "submitted_at": null,
                          "reviewed_at": null,
                          "review_notes": null,
                          "updated_at": "2026-06-11T16:30:00Z",
                          "created_at": "2026-06-10T08:00:00Z",
                          "is_offline_submission": false
                        },
                        {
                          "id": 2232,
                          "form_id": 12,
                          "form_name": "Site Safety Audit",
                          "form_description": "Quarterly site-level safety walkthrough",
                          "form_category": "safety",
                          "status": "submitted",
                          "status_label": "Submitted",
                          "completion_percentage": 100,
                          "submitted_at": "2026-06-12T09:52:00Z",
                          "reviewed_at": null,
                          "review_notes": null,
                          "updated_at": "2026-06-12T09:52:00Z",
                          "created_at": "2026-06-12T09:00:00Z",
                          "is_offline_submission": false
                        }
                      ],
                      "meta": {
                        "total_count": 6,
                        "current_page": 1,
                        "total_pages": 1,
                        "per_page": 20
                      },
                      "segment_counts": {
                        "all": 6,
                        "draft": 1,
                        "submitted": 2,
                        "under_review": 1,
                        "approved": 1,
                        "rejected": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "description": "Invalid status value",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "invalid_status"
                    },
                    "message": {
                      "type": "string",
                      "example": "status must be one of: draft, submitted, under_review, approved, rejected"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/forms/assignments": {
      "get": {
        "tags": [
          "Forms"
        ],
        "summary": "List the caller's open recurring-form assignments",
        "description": "Returns the current user's open form assignments — the mobile **Assigned**\ntab. Caller-scoped: never another user's assignments, never tenant-wide.\n\n**Derived, not stored.** There is no per-user assignment row in the Forms\nschema. A scheduled form only sends notifications when it runs; it does not\nwrite an assignment record or a `pending_completion` submission. An\n\"assignment\" here is therefore computed as an active schedule that names the\ncaller, whose template is still accepting submissions, and whose audience\nstill includes the caller — joined to the caller's own draft for that\ntemplate.\n\n**Stale assignee lists are filtered out.** A schedule's assignee list is an\nadmin-authored blob written when the schedule was saved and never\nrevalidated, so it still names people who have since moved out of the target\ndepartment, location, or group. This endpoint applies the **same\ndelivery-time audience re-check** the scheduling job applies, so it lists\nonly assignments the job itself would actually deliver.\n\nExcluded: paused schedules, schedules past their end date, and templates that\nare archived, unpublished, closed by schedule, or at their response cap.\n\n`403` when the Forms app is not enabled or not licensed for the tenant,\nmatching the sibling Forms reads.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Clamped to 1..50.",
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's open assignments",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "description": "Scheduled-form id — the assignment's identity."
                          },
                          "form_id": {
                            "type": "integer"
                          },
                          "form_name": {
                            "type": "string"
                          },
                          "form_description": {
                            "type": "string",
                            "nullable": true
                          },
                          "form_category": {
                            "type": "string",
                            "nullable": true
                          },
                          "priority": {
                            "type": "string",
                            "nullable": true
                          },
                          "page_count": {
                            "type": "integer"
                          },
                          "field_count": {
                            "type": "integer"
                          },
                          "required_field_count": {
                            "type": "integer"
                          },
                          "schedule_descriptor": {
                            "type": "string",
                            "description": "Human cadence, e.g. \"Scheduled · every 2 weeks\".",
                            "example": "Scheduled · weekly"
                          },
                          "due_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "description": "When the current cycle closes. The schema holds no\nper-occurrence due date, so this is the schedule's next\nreset — the deadline for \"the check you do this cycle\".\nThe campaign's terminal bound is `ends_at`, kept separate\nso a weekly form is not reported as due months away.\n"
                          },
                          "next_run_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "last_run_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "ends_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "description": "Schedule's terminal bound, or null when open-ended."
                          },
                          "draft_submission_id": {
                            "type": "integer",
                            "nullable": true,
                            "description": "The caller's draft for this template, or null."
                          },
                          "completion_percentage": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Null when not started."
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "due_today",
                              "assigned_to_you",
                              "action_needed"
                            ],
                            "description": "`action_needed` when a draft exists (that outranks the\ndate), else `due_today` when the cycle closes today,\nelse `assigned_to_you`.\n"
                          },
                          "action": {
                            "type": "string",
                            "enum": [
                              "start",
                              "continue_draft"
                            ]
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "total_count": {
                          "type": "integer"
                        },
                        "current_page": {
                          "type": "integer"
                        },
                        "total_pages": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": 73,
                      "form_id": 799,
                      "form_name": "Employee Information Update",
                      "form_description": "Request changes to your personal information on file",
                      "form_category": "hr",
                      "priority": "normal",
                      "page_count": 1,
                      "field_count": 4,
                      "required_field_count": 0,
                      "schedule_descriptor": "Scheduled · weekly",
                      "due_at": "2026-08-17T19:46:49Z",
                      "next_run_at": "2026-08-17T19:46:49Z",
                      "last_run_at": "2026-08-10T19:46:49Z",
                      "ends_at": "2026-12-12T19:46:49Z",
                      "draft_submission_id": null,
                      "completion_percentage": null,
                      "status": "assigned_to_you",
                      "action": "start"
                    }
                  ],
                  "meta": {
                    "total_count": 1,
                    "current_page": 1,
                    "total_pages": 1,
                    "per_page": 20
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/forms/categories": {
      "get": {
        "tags": [
          "Forms"
        ],
        "summary": "List form categories with published-form counts",
        "description": "Returns the canonical list of form categories (the same set the web form\nbuilder and Published Forms filter use), each with the number of published\nforms in that category for the caller's business.\n\n- The full canonical list is always returned, including categories with `form_count: 0`\n- Items follow the platform's defined category order (not sorted by count or name)\n- `form_count` uses the same exclusions as `GET /api/v1/forms`\n  (published only; excludes survey-category and survey-linked forms), so a\n  category's `form_count` equals the `meta.total_count` of\n  `GET /api/v1/forms?category=<value>`\n- Categories are not tenant-configurable today\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Categories retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FormCategoryItem"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Number of categories returned (the full canonical list)",
                          "example": 14
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "default": {
                    "summary": "Categories with published-form counts",
                    "value": {
                      "items": [
                        {
                          "value": "general",
                          "label": "General",
                          "form_count": 4
                        },
                        {
                          "value": "safety",
                          "label": "Safety",
                          "form_count": 2
                        },
                        {
                          "value": "hr",
                          "label": "HR",
                          "form_count": 6
                        },
                        {
                          "value": "it",
                          "label": "IT",
                          "form_count": 0
                        }
                      ],
                      "meta": {
                        "total_count": 14
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/forms/approvals": {
      "get": {
        "tags": [
          "Forms"
        ],
        "summary": "List submissions pending the current reviewer's approval",
        "description": "Returns form submissions awaiting review/approval — the mobile\n**Approvals** queue. Mirrors the web Submissions review queue.\n\n- **Reviewer-only.** The caller must be an admin, manager, or Forms App\n  Admin in the business; everyone else gets `403`.\n- **What each reviewer sees differs by role:**\n  - **Admins and Forms App Admins** see the business-wide\n    **pending_review** set (statuses `submitted` and `under_review`).\n  - **Managers** see that **same** status set, narrowed to submissions\n    from their own **direct reports** (active members of the business who\n    report to them). A manager who is also an admin or Forms App Admin is\n    treated as an admin (unrestricted).\n- The multi-step approval-workflow model is not yet wired up, so there is\n  no per-step / per-approver assignment beyond this role scoping.\n- Ordered **oldest-waiting-first** (longest in the queue at the top).\n- The home-screen pending-approval badge (`GET /api/v1/home` →\n  `forms.pending_approval_count`) counts exactly this same per-role set.\n\n**Filters.** All are optional and compose with `AND`; omitting every one\nreturns the unfiltered queue. They narrow the caller's already-authorized\nqueue and can never widen it — `?user_id=` on a manager's queue can only\npick one of their own reportees. A value that cannot be a filter (an\nunparseable date, an array/hash-shaped `?template_id[a]=b`) is **dropped**\nfrom the query: the response is a `200` with that filter simply not\napplied, never a `500` and never a silently-empty list.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "overdue",
            "in": "query",
            "required": false,
            "description": "`true` returns only submissions that have been awaiting review longer\nthan the server-side review SLA (currently 7 days, measured on\n`submitted_at` falling back to `created_at`). Identical to the web\nreview queue's Overdue filter. Do not hardcode the threshold on the\nclient — read the per-item `overdue` flag instead.\n",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "template_id",
            "in": "query",
            "required": false,
            "description": "Only submissions of this form template",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "user_id",
            "in": "query",
            "required": false,
            "description": "Only submissions from this submitter",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "date_from",
            "in": "query",
            "required": false,
            "description": "Inclusive start of the submitted-date window (`YYYY-MM-DD`), measured\non the same clock as `submitted_at` (falling back to `created_at`).\n",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "date_to",
            "in": "query",
            "required": false,
            "description": "Inclusive end of the submitted-date window (`YYYY-MM-DD`)",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Case-insensitive match on the form name OR the submitter's name",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number (default 1)",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Items per page, 1–50 (default 20)",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Pending approvals retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ApprovalItem"
                      }
                    },
                    "meta": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/PaginationMeta"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "overdue_count": {
                              "type": "integer",
                              "description": "Overdue submissions in the whole filtered set (not just\nthis page) — badge the queue and populate an \"Overdue\"\ntile from this one call.\n",
                              "example": 4
                            }
                          }
                        }
                      ]
                    }
                  }
                },
                "examples": {
                  "queue": {
                    "summary": "Reviewer's pending queue",
                    "value": {
                      "items": [
                        {
                          "id": 2231,
                          "reference": "#2231",
                          "form_id": 7,
                          "form_name": "Site Safety Audit",
                          "form_description": "Quarterly site-level safety walkthrough",
                          "form_category": "safety",
                          "priority": "high",
                          "submitter": "J. Rivera",
                          "submitter_email": "jrivera@example.com",
                          "submitter_photo_url": "https://officechat-dev.workforce.mangoapps.com/rails/active_storage/.../avatar.jpg",
                          "status": "under_review",
                          "status_label": "Under review",
                          "requires_approval": true,
                          "overdue": true,
                          "submitted_at": "2026-06-18T09:00:00Z",
                          "waiting_since": "2026-06-18T09:00:00Z",
                          "updated_at": "2026-06-18T09:05:00Z",
                          "created_at": "2026-06-18T08:40:00Z"
                        }
                      ],
                      "meta": {
                        "total_count": 4,
                        "current_page": 1,
                        "total_pages": 1,
                        "per_page": 20,
                        "overdue_count": 4
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/forms/{id}": {
      "get": {
        "tags": [
          "Forms"
        ],
        "summary": "Get form template for mobile rendering",
        "description": "Returns complete form template with fields optimized for mobile rendering.\nIncludes a draft submission for offline support and mobile-specific configuration.\n\nThe response includes all data needed to render the form offline and handle submissions.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Form template ID",
            "schema": {
              "type": "integer",
              "example": 123
            }
          },
          {
            "name": "submission_id",
            "in": "query",
            "required": false,
            "description": "Chooses the source for each field's `submission_data` value (always present in the response). When supplied, values come from THIS submission (for resume/edit) — scoped to the caller's OWN submissions of this form; a blank, missing, or other-user submission_id yields `null`. When omitted, values come from the caller's DRAFT submission for this form; if there is no draft data, every field's `submission_data` is `null`.",
            "schema": {
              "type": "integer",
              "example": 789
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Form template retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Form data optimized for rendering",
                  "properties": {
                    "template": {
                      "$ref": "#/components/schemas/FormTemplate"
                    },
                    "draft_submission": {
                      "$ref": "#/components/schemas/FormSubmission",
                      "nullable": true,
                      "description": "Existing draft submission for progress restoration. NOTE: the `submission_data` property is intentionally omitted here — each field in `fields[]` now carries its own `submission_data` (sourced from this draft or the requested submission), so the map is not repeated on this object."
                    },
                    "mobile_config": {
                      "type": "object",
                      "description": "Mobile-specific rendering configuration",
                      "properties": {
                        "offline_capable": {
                          "type": "boolean",
                          "description": "Whether the form supports offline mode",
                          "example": true
                        },
                        "voice_input_enabled": {
                          "type": "boolean",
                          "description": "Whether voice input is available",
                          "example": true
                        },
                        "photo_capture_enabled": {
                          "type": "boolean",
                          "description": "Whether photo capture is available",
                          "example": true
                        },
                        "gps_enabled": {
                          "type": "boolean",
                          "description": "Whether GPS location is required",
                          "example": false
                        },
                        "estimated_time_minutes": {
                          "type": "integer",
                          "description": "Estimated completion time",
                          "example": 10
                        }
                      }
                    },
                    "prefill_data": {
                      "type": "object",
                      "description": "Pre-filled field values",
                      "nullable": true,
                      "additionalProperties": true
                    }
                  },
                  "required": [
                    "template",
                    "mobile_config"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/forms/{id}/preview": {
      "get": {
        "tags": [
          "Forms"
        ],
        "summary": "Preview a form (lightweight metadata + field list)",
        "description": "Lightweight preview of a form: header metadata plus the ordered field\nlist with **label and type only** — no field configuration, validation,\noptions, draft, or per-user state, and (unlike `GET /forms/{id}`) it does\nNOT create a draft submission. Use it to show a form's shape before the\nuser opens it to fill.\n\nAccess is gated like `GET /forms/{id}` (the Forms app must be enabled for\nthe business and visible to the user — the same access model as the web),\nso the field structure of an inaccessible form is not exposed.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Form template ID",
            "schema": {
              "type": "integer",
              "example": 123
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Form preview retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer",
                      "example": 123
                    },
                    "name": {
                      "type": "string",
                      "example": "Site Safety Audit"
                    },
                    "category": {
                      "type": "string",
                      "nullable": true,
                      "example": "operations"
                    },
                    "page_count": {
                      "type": "integer",
                      "description": "Number of pages (page_break count + 1)",
                      "example": 2
                    },
                    "field_count": {
                      "type": "integer",
                      "description": "Total number of fields (equals the size of `fields`)",
                      "example": 12
                    },
                    "estimated_time_minutes": {
                      "type": "integer",
                      "description": "Estimated time to complete, in minutes",
                      "example": 8
                    },
                    "shareable_link": {
                      "type": "string",
                      "nullable": true,
                      "description": "Public share URL (`/f/{token}`) for the form, or `null` when the form is not publicly accessible (not published, sharing disabled, public access off, or no token issued).\n",
                      "example": "https://officechat.workforce.mangoapps.com/f/abc123"
                    },
                    "qr_code_image_url": {
                      "type": "string",
                      "nullable": true,
                      "description": "Absolute URL of the form's QR-code image (PNG), encoding the public share link. Present only when the form is publicly accessible (same gate as `shareable_link`); `null` otherwise. Keyed by the form's public access token (not its numeric id) so a mobile client with no web session can load it directly.\n",
                      "example": "https://officechat.workforce.mangoapps.com/apps/forms/templates/Xlv6kQQt1RSiHKB0kQUsiPmLcVm_l2Cu/qr_code?format=png"
                    },
                    "fields": {
                      "type": "array",
                      "description": "All fields in display (position) order, each with label and type only. No other field information is included.\n",
                      "items": {
                        "type": "object",
                        "properties": {
                          "label": {
                            "type": "string",
                            "nullable": true,
                            "description": "Field label (null for unlabeled fields such as page breaks)",
                            "example": "Full Name"
                          },
                          "type": {
                            "type": "string",
                            "description": "Field type",
                            "example": "text"
                          }
                        },
                        "required": [
                          "type"
                        ]
                      },
                      "example": [
                        {
                          "label": "Full Name",
                          "type": "text"
                        },
                        {
                          "label": "Site",
                          "type": "select"
                        },
                        {
                          "label": null,
                          "type": "page_break"
                        },
                        {
                          "label": "Notes",
                          "type": "textarea"
                        }
                      ]
                    }
                  },
                  "required": [
                    "id",
                    "name",
                    "page_count",
                    "field_count",
                    "estimated_time_minutes",
                    "fields"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/forms/{id}/submit": {
      "post": {
        "tags": [
          "Forms"
        ],
        "summary": "Submit completed form",
        "description": "Submit a completed form, either as a new submission or sync from offline mode.\nSupports both immediate submission and offline sync scenarios.\n\nFor offline sync, include submission_id and set offline_sync=true.\n\nMedia fields (file / image / video / audio / signature / gallery): direct-\nupload each file first via the standard ActiveStorage endpoint\n(POST /rails/active_storage/direct_uploads) to obtain a blob `signed_id`,\nthen embed that `signed_id` in `submission_data` for the field — a string\nfor a single field, an array for a gallery. The server validates all\nreferences (all-or-nothing, 422 on any bad/expired id or unsupported\ntype/size), creates the file records, and rewrites those keys to a\n`{ uploaded, file_id, filename, content_type, file_size }` reference.\nValues already in reference form are left as-is.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Form template ID",
            "schema": {
              "type": "integer",
              "example": 123
            }
          },
          {
            "name": "submission_id",
            "in": "query",
            "required": false,
            "description": "Existing draft submission ID (for offline sync)",
            "schema": {
              "type": "integer",
              "example": 456
            }
          },
          {
            "name": "offline_sync",
            "in": "query",
            "required": false,
            "description": "Indicates this is an offline sync operation",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "submission": {
                    "type": "object",
                    "properties": {
                      "submission_data": {
                        "type": "object",
                        "description": "Form field values as key-value pairs",
                        "example": {
                          "employee_name": "John Doe",
                          "incident_date": "2024-01-15",
                          "description": "Equipment malfunction in warehouse"
                        }
                      }
                    }
                  },
                  "device_id": {
                    "type": "string",
                    "description": "Mobile device identifier",
                    "example": "mobile-123abc"
                  }
                },
                "required": [
                  "submission"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Form submitted successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Form submitted successfully"
                    },
                    "submission": {
                      "$ref": "#/components/schemas/FormSubmission"
                    },
                    "sync_status": {
                      "type": "string",
                      "enum": [
                        "immediate",
                        "synced"
                      ],
                      "example": "immediate"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/forms/{id}/submissions": {
      "post": {
        "tags": [
          "Forms"
        ],
        "summary": "Create or update draft submission",
        "description": "Create a new draft submission or update an existing one for offline support.\nThis endpoint allows mobile apps to save form progress locally and sync later.\n\nPass `offline_sync=true` ONLY when saving a draft that was composed while\nthe device was offline; an online client should omit it (or send false) so\nthe draft is not mislabeled as an offline submission. On an existing draft\nthe flag can only be turned on — an online edit never downgrades a draft\nthat was genuinely created offline.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Form template ID",
            "schema": {
              "type": "integer",
              "example": 123
            }
          },
          {
            "name": "offline_sync",
            "in": "query",
            "required": false,
            "description": "Set true only when saving a draft created while offline. Defaults to online (false).",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "submission": {
                    "type": "object",
                    "properties": {
                      "submission_data": {
                        "type": "object",
                        "description": "Partial form field values",
                        "example": {
                          "employee_name": "John Doe",
                          "incident_date": "2024-01-15"
                        }
                      }
                    }
                  }
                },
                "required": [
                  "submission"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Draft submission created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Draft saved successfully"
                    },
                    "submission": {
                      "$ref": "#/components/schemas/FormSubmission"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "200": {
            "description": "Draft submission updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Draft saved successfully"
                    },
                    "submission": {
                      "$ref": "#/components/schemas/FormSubmission"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/forms/submissions/{id}": {
      "delete": {
        "tags": [
          "Forms"
        ],
        "summary": "Delete a draft submission",
        "description": "Delete a form submission. Two gates apply, in order:\n\n1. **Owner-only** — only the user who CREATED the submission may delete\n   it (a reviewer/admin cannot delete someone else's draft here) →\n   `403` with error code `access_denied`.\n2. **Draft-only** — the submission must still be a `draft`. Once it is\n   submitted / under_review / approved / rejected it is part of the\n   review record and cannot be deleted → `422` with error code\n   `cannot_delete` (the current `status` is returned in `error.details`).\n\nDeleting cascades to the submission's uploaded files. Requires the\n`write:forms` scope. (The same submission's GET and PATCH are also\navailable at `/form_submissions/{id}`.)\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Form submission ID",
            "schema": {
              "type": "integer",
              "example": 456
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Draft submission deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Draft submission deleted"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/form_submissions/{id}": {
      "get": {
        "tags": [
          "Forms"
        ],
        "summary": "Get submission details",
        "description": "Get details of a specific form submission including all field values and files.\nUsed to view completed submissions and track submission status.\n\nAccess mirrors the web Submissions surface: the submission's owner, or any\nadmin / manager / Forms App Admin in the business, may view it. Any other\nuser receives 403. Sensitive (manager-/submitter-restricted) field values\nare stripped from `submission_data` for viewers not permitted to see them.\n\n**submission_data shape (this endpoint only):** unlike the write/draft\nendpoints (which return a flat `field_name → value` map), the detail\nresponse expands each answer into an object carrying the field's `label`,\nsubmitted `value`, and `type` — `{ field_name: { label, value, type } }`.\nKeys are ordered by the template's field position (matching the web detail\nview), with any orphaned data (no matching field) appended last.\nThe response's `submission.submitted_by` carries the submitter's name + email + photo_url.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Form submission ID",
            "schema": {
              "type": "integer",
              "example": 456
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Submission retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "submission": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/FormSubmission"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "submitted_by": {
                              "type": "object",
                              "nullable": true,
                              "description": "Who submitted the form — name + email + photo. Resolves to the respondent email (or \"Anonymous User\") for anonymous submissions. photo_url is the submitter's thumbnail avatar (a ui-avatars fallback when no photo is set), or null for anonymous submissions.\n",
                              "properties": {
                                "name": {
                                  "type": "string",
                                  "nullable": true
                                },
                                "email": {
                                  "type": "string",
                                  "nullable": true
                                },
                                "photo_url": {
                                  "type": "string",
                                  "nullable": true
                                }
                              }
                            },
                            "submission_data": {
                              "type": "object",
                              "description": "Per-field answers keyed by field_name. Each value is an object with the field's label, submitted value, and type (the expanded detail shape, not the flat write-endpoint map).\n",
                              "additionalProperties": {
                                "type": "object",
                                "properties": {
                                  "label": {
                                    "type": "string",
                                    "nullable": true,
                                    "description": "Field label from the template definition"
                                  },
                                  "value": {
                                    "nullable": true,
                                    "description": "Submitted value — a string or array for simple fields; a file-reference object/array for media fields (each reference includes a `url` to view/download the file); and, for `matrix` and Likert-mode `scale` fields, an ORDERED array of { label, value } pairs where `label` is the creator's row/statement label and `value` is the chosen column/option label (an array of labels for a checkbox-grid or multiple-select matrix row). Pairs are returned in the creator's configured row/statement order. A range-mode `scale` returns a plain scalar.\n"
                                  },
                                  "type": {
                                    "type": "string",
                                    "nullable": true,
                                    "description": "Field type (text, image, gallery, signature, select, …)"
                                  }
                                }
                              },
                              "example": {
                                "site_photo": {
                                  "label": "Site photo",
                                  "type": "image",
                                  "value": {
                                    "uploaded": true,
                                    "file_id": 36,
                                    "filename": "photo.png",
                                    "content_type": "image/png",
                                    "file_size": 20480,
                                    "url": "/rails/active_storage/blobs/redirect/eyJf…/photo.png"
                                  }
                                },
                                "notes": {
                                  "label": "Notes",
                                  "type": "textarea",
                                  "value": "All clear on the east wing."
                                }
                              }
                            }
                          }
                        }
                      ]
                    },
                    "form": {
                      "type": "object",
                      "description": "Summary information for a form template",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "description": "Unique form template ID",
                          "example": 123
                        },
                        "name": {
                          "type": "string",
                          "description": "Form name",
                          "example": "Incident Report"
                        },
                        "description": {
                          "type": "string",
                          "description": "Form description",
                          "example": "Report workplace incidents and safety concerns"
                        },
                        "category": {
                          "type": "string",
                          "description": "Form category",
                          "example": "safety"
                        },
                        "priority": {
                          "type": "string",
                          "enum": [
                            "urgent",
                            "high",
                            "normal",
                            "low"
                          ],
                          "description": "Form priority level",
                          "example": "high"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "published",
                            "archived"
                          ],
                          "description": "Template status",
                          "example": "published"
                        },
                        "field_count": {
                          "type": "integer",
                          "description": "Total number of fields in the form",
                          "example": 8
                        },
                        "required_field_count": {
                          "type": "integer",
                          "description": "Number of required fields",
                          "example": 5
                        },
                        "estimated_time_minutes": {
                          "type": "integer",
                          "description": "Estimated completion time in minutes",
                          "example": 10
                        },
                        "has_file_uploads": {
                          "type": "boolean",
                          "description": "Whether the form supports file uploads",
                          "example": true
                        },
                        "sharing_enabled": {
                          "type": "boolean",
                          "description": "Whether the form can be shared publicly",
                          "example": false
                        },
                        "requires_approval": {
                          "type": "boolean",
                          "description": "Whether submissions require approval",
                          "example": true
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "Form creation timestamp",
                          "example": "2024-01-15T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "Last update timestamp",
                          "example": "2024-01-15T10:00:00Z"
                        }
                      },
                      "required": [
                        "id",
                        "name",
                        "category",
                        "priority",
                        "status",
                        "field_count",
                        "estimated_time_minutes"
                      ]
                    },
                    "timeline": {
                      "type": "array",
                      "description": "Status timeline shown on the web \"View details\" page. Always begins\nwith a \"Form Submitted\" event, followed by a single review event\nreflecting the current state (approved / fields returned for\ncorrection / rejected) when reviewed.\n",
                      "items": {
                        "$ref": "#/components/schemas/SubmissionTimelineEvent"
                      }
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Forbidden — not the owner and not an admin/manager/Forms App Admin"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "tags": [
          "Forms"
        ],
        "summary": "Update draft submission",
        "description": "Update a draft form submission for offline editing and progress saving.\nOnly draft submissions can be updated.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Form submission ID",
            "schema": {
              "type": "integer",
              "example": 456
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "submission": {
                    "type": "object",
                    "properties": {
                      "submission_data": {
                        "type": "object",
                        "description": "Updated form field values",
                        "example": {
                          "employee_name": "John Doe",
                          "incident_date": "2024-01-15",
                          "description": "Updated description"
                        }
                      }
                    }
                  }
                },
                "required": [
                  "submission"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Draft submission updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Draft updated successfully"
                    },
                    "submission": {
                      "$ref": "#/components/schemas/FormSubmission"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/form_submissions/{id}/approve": {
      "post": {
        "tags": [
          "Forms"
        ],
        "summary": "Approve a form submission",
        "description": "Approve a submission that is awaiting review, mirroring the web\n**Approval** flow (Approve action) and the native Approvals screen\n(swipe-right / \"Approve\").\n\n**Authorization:** reviewer only — the caller must be an admin, manager,\nor Forms App Admin for the business (`write:forms` scope). A plain member,\n*including the submission's own owner*, receives `403`.\n\n**Preconditions:**\n- The submission must be **awaiting review** (`submitted` or `under_review`),\n  else `422` with code `invalid_state`.\n- Approval is blocked while any field is still returned to the employee for\n  field-level correction (`422` with code `open_returns`).\n\nSets `status = approved`, stamps `reviewed_by` / `reviewed_at`, and\noptionally records `review_notes`. The submitter is emailed a\nstatus-changed notification (best-effort).\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Form submission ID",
            "schema": {
              "type": "integer",
              "example": 456
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "review_notes": {
                    "type": "string",
                    "description": "Optional approval note recorded on the submission.",
                    "example": "Approved — all required documents attached."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Submission approved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Submission approved"
                    },
                    "submission": {
                      "$ref": "#/components/schemas/ReviewedSubmission"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Forbidden — token lacks `write:forms` scope (`insufficient_permissions`)\nor the caller is not a reviewer (`access_denied`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Submission is not awaiting review (`invalid_state`) or fields are still\nreturned to the employee (`open_returns`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/form_submissions/{id}/reject": {
      "post": {
        "tags": [
          "Forms"
        ],
        "summary": "Reject a form submission",
        "description": "Reject a submission that is awaiting review, mirroring the web reject\nmodal and the native reject-note bottom sheet (swipe-left / \"Reject…\").\n\n**Authorization:** reviewer only — the caller must be an admin, manager,\nor Forms App Admin for the business (`write:forms` scope). A plain member,\n*including the submission's own owner*, receives `403`.\n\n**Preconditions:**\n- `review_notes` (the rejection reason) is **required**; a missing or\n  blank/whitespace-only value returns `422` with code `review_notes_required`.\n  The note is shown to the submitter so they can correct and resubmit.\n- The submission must be **awaiting review** (`submitted` or `under_review`),\n  else `422` with code `invalid_state`.\n\nSets `status = rejected`, stamps `reviewed_by` / `reviewed_at`, and stores\n`review_notes`. The submitter is emailed a status-changed notification\n(best-effort).\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Form submission ID",
            "schema": {
              "type": "integer",
              "example": 456
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "review_notes": {
                    "type": "string",
                    "description": "Reason for rejection (required). Shown to the submitter.",
                    "example": "The incident date is missing — please add it and resubmit."
                  }
                },
                "required": [
                  "review_notes"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Submission rejected",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Submission rejected"
                    },
                    "submission": {
                      "$ref": "#/components/schemas/ReviewedSubmission"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Forbidden — token lacks `write:forms` scope (`insufficient_permissions`)\nor the caller is not a reviewer (`access_denied`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "`review_notes` is missing or blank (`review_notes_required`), or the\nsubmission is not awaiting review (`invalid_state`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/form_submissions/{id}/return_fields": {
      "post": {
        "tags": [
          "Forms"
        ],
        "summary": "Return individual fields for correction",
        "description": "The reviewer's **third option**, alongside approve and reject: return\nNAMED fields to the submitter with a per-field comment, instead of\nrejecting a whole submission over a couple of bad answers. The submitter\nthen corrects only those fields — everything else stays locked and\nanswered.\n\nShares its implementation with the web reviewer action\n(`PATCH /apps/forms/submissions/:id/return_fields`) — same returnable-field\nrules, same preconditions, same notifications — so the two surfaces cannot\ndrift on what a return does.\n\n**Authorization:** reviewer only — the caller must be an admin, manager\n(of the submitter), Forms App Admin, or the form's owner, and the token\nmust carry `write:forms`. A plain member, *including the submission's own\nowner*, receives `403`. This is the same reviewer set the rest of this\nAPI applies to a submission (read, approve, reject); it does not include\na user named as approver of the submission's current workflow step, who\ncan act on it from the web only.\n\n**Preconditions:**\n- The submission must be **awaiting review** (`submitted` or `under_review`),\n  else `422` with code `not_awaiting_review`.\n- The submission must have a submitter. An **anonymous** submission (public\n  portal) has nobody to return fields to and returns `422` with code\n  `anonymous_submission` — approve or reject it instead.\n- At least one **returnable** field must carry a **non-blank comment**,\n  else `422` with code `no_returnable_fields`.\n\n**Fields that are never returnable** — dropped server-side, never applied,\nand named back in `ignored_fields`:\n- file / media fields (`file`, `image`, `video`, `audio`, `gallery`) and\n  `annotation` — the corrections form cannot re-process an upload\n- any field this reviewer is not permitted to READ (field-level visibility)\n- any name that is not a field on the form\n- any field supplied with a blank comment\n\n**Effects:** `status` → `changes_requested`, `review_round` incremented by\n1, a `field_reviews` entry written per returned field (state / comment /\nround / history), and `reviewed_by` / `reviewed_at` stamped. The submitter\nreceives an in-app *action required* notification **and** an email\n(best-effort — a delivery failure never fails the call).\n\nA subsequent `GET /api/v1/forms/{form_id}?submission_id={id}` reflects the\nnew `returned_field_names`, each field's `field_review`, and\n`is_editable: false` on the fields that were not returned.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Form submission ID",
            "schema": {
              "type": "integer",
              "example": 4812
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "field_returns": {
                    "type": "object",
                    "description": "`{ field_name: comment }`. The comment is REQUIRED per field\nand is shown to the submitter — a blank comment does not\nreturn the field.\n",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "example": {
                      "incident_location": "Please give the bay number, not just 'warehouse'.",
                      "witness_name": "Left blank — required if anyone else was present."
                    }
                  },
                  "review_notes": {
                    "type": "string",
                    "description": "Optional submission-level note recorded on the submission.",
                    "example": "Two fields need detail before I can approve."
                  }
                },
                "required": [
                  "field_returns"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Fields returned to the submitter for correction",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Returned 2 fields for correction"
                    },
                    "submission": {
                      "$ref": "#/components/schemas/ReviewedSubmission"
                    },
                    "ignored_fields": {
                      "type": "array",
                      "description": "Names the request asked to return that the server dropped —\nnon-returnable, unknown, or supplied with a blank comment.\n**Omitted entirely when nothing was dropped.** Same key and\nsame meaning as on the submission write paths.\n",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "site_photo"
                      ]
                    },
                    "warning": {
                      "type": "string",
                      "description": "Human-readable companion to `ignored_fields`; omitted when nothing was dropped."
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Forbidden — token lacks `write:forms` scope (`insufficient_permissions`)\nor the caller is not a reviewer (`access_denied`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Submission is not awaiting review (`not_awaiting_review`), was submitted\nanonymously (`anonymous_submission`), no returnable field carried a\ncomment (`no_returnable_fields`, whose `details.ignored_fields` names\nwhat was dropped), or the return could not be saved (`return_failed`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/marketplace_apps": {
      "get": {
        "tags": [
          "Marketplace Apps"
        ],
        "summary": "List enabled marketplace apps",
        "description": "Returns a list of marketplace apps that are enabled for the authenticated user's business.\nThis endpoint provides all data needed to display a grid view of apps in a mobile client,\nincluding app icons, names, descriptions, and launch URLs.\n\nApps are returned in display order (sort_order) and include configuration data\nspecific to the business.\n",
        "parameters": [
          {
            "name": "include_metadata",
            "in": "query",
            "required": false,
            "description": "Whether to include app metadata in response",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Enabled marketplace apps retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "apps": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MarketplaceApp"
                      }
                    },
                    "total_count": {
                      "type": "integer",
                      "description": "Total number of enabled apps",
                      "example": 5
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/core_apps": {
      "get": {
        "tags": [
          "System Modules"
        ],
        "summary": "List enabled core apps",
        "description": "Returns a list of core platform apps and their enabled status for the authenticated user's business.\nCore apps are fundamental platform features like Shifts & Scheduling, Time & Attendance, etc.\nthat can be enabled or disabled at the business level.\n\nThis endpoint provides all data needed to display core app status in a mobile client,\nincluding app icons, descriptions, feature lists, and direct URLs.\n",
        "responses": {
          "200": {
            "description": "Core apps status retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "apps": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CoreApp"
                      }
                    },
                    "total_count": {
                      "type": "integer",
                      "description": "Total number of core apps",
                      "example": 6
                    },
                    "enabled_count": {
                      "type": "integer",
                      "description": "Number of enabled core apps",
                      "example": 3
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/apps": {
      "get": {
        "tags": [
          "Apps"
        ],
        "summary": "List all apps (core and marketplace)",
        "description": "**Consolidated endpoint** that returns both core apps and marketplace apps in a single response.\nThis is the recommended endpoint for mobile clients to build the app grid view.\n\nCore apps are fundamental platform features (Shifts & Scheduling, Time & Attendance, etc.)\nwhile marketplace apps are add-on features that can be enabled/disabled.\n\n**Features:**\n- Single API call instead of multiple requests\n- Unified sorting and filtering across all app types\n- Category-based organization\n- Type filtering (core, marketplace, or all)\n- Enabled/disabled filtering\n- **Mobile native app exclusion** via `exclude_mobile_native` parameter\n\n**Response includes:**\n- App icons (Font Awesome classes for mobile, with web_url fallback for custom SVG icons)\n- Launch URLs\n- Feature lists\n- Metadata\n- Category grouping\n\n**Icon Handling for Mobile:**\nAll apps now return Font Awesome 5 icon classes in `icon.url` with `icon.type: \"icon_class\"`.\nFor apps that have custom SVG icons, the original SVG path is available in `icon.web_url`\nfor web clients that prefer to use the custom icons.\n\n**Mobile Native Apps:**\nUse `exclude_mobile_native=true` to exclude apps that are natively supported in mobile clients:\n- shift-marketplace\n- shifts_scheduling\n- time_attendance\n- leave_management\n- timesheets\n",
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Filter apps by type (core, marketplace, or all)",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "core",
                "marketplace"
              ],
              "default": "all"
            },
            "example": "all"
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Filter apps by category (e.g., \"HR Management\", \"Workforce Management\")",
            "schema": {
              "type": "string"
            },
            "example": "Workforce Management"
          },
          {
            "name": "enabled_only",
            "in": "query",
            "required": false,
            "description": "Whether to return only enabled apps",
            "schema": {
              "type": "boolean",
              "default": true
            },
            "example": true
          },
          {
            "name": "exclude_mobile_native",
            "in": "query",
            "required": false,
            "description": "Exclude apps that are natively supported in mobile clients.\nWhen set to true, the following apps are excluded:\n- shift-marketplace\n- shifts_scheduling\n- time_attendance\n- leave_management\n- timesheets\n\nThis is useful for mobile clients that already have native implementations\nof these core features and only need to display marketplace/web-based apps.\n",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "example": false
          },
          {
            "name": "include_navigation",
            "in": "query",
            "required": false,
            "description": "Opt into the **grouped, navigation-aware** response shape. Off by\ndefault so existing clients keep the flat legacy payload described\nabove — this parameter changes the TOP-LEVEL shape of the response,\nit does not merely add a field.\n\nWhen `include_navigation=true`:\n\n* The response is split into `pinned_apps` (the apps pinned for this\n  user) and `apps` (the remaining enabled apps), with `pinned_count`\n  / `apps_count` / `total_count` / `categories`. `core_count`,\n  `marketplace_count` and `enabled_count` are **not** returned in\n  this mode.\n* Every app object carries `pinned`, `has_mobile_view`, `mobile_url`\n  and a `navigation_items` array — the app's in-app navigation\n  (its sidebar tabs / mobile TabBar), so a client can render an app's\n  sub-pages without a second call per app.\n* `enabled_only` and `type` are **ignored**: pinned/unpinned lists\n  come from the web sidebar's own role-aware source\n  (`SidebarHelper#pinned_apps_for_user` / `#unpinned_enabled_apps`),\n  which only ever yields enabled, user-accessible marketplace/core\n  apps. `category` and `exclude_mobile_native` are still applied.\n* For mobile clients (native `User-Agent`), apps without a\n  mobile-optimized view are dropped entirely, and each item's `path`\n  is rewritten to its `/m/...` equivalent (see `navigation_items`).\n\n`navigation_items` is **role-aware and gated per app** — it contains\nonly what the caller may actually open, so an item's absence is the\nauthorization signal. Two examples of the gating, straight from the\nbuilders:\n\n* **Ideas** always returns `dashboard` (`/apps/ideas`) and\n  `all_ideas` (`/apps/ideas/list`); `campaigns` appears unless the\n  business explicitly disabled campaigns\n  (`configuration[\"campaigns\"] == false`); `review_queue`\n  (\"Reviews\") appears **only** for members of a review panel — the\n  workspace default panel or a campaign panel. Panel membership is\n  the only grant: **admins get no bypass**, matching the web\n  \"My Review Queue\" tab.\n* **Forms** always returns `my_submissions`; `approvals` appears only\n  for reviewers (admin / manager / Forms App Admin) and carries a\n  live `count` of pending approvals.\n* **Communications** returns a fixed four-item set with no gating:\n  `dashboard` (`/apps/communications`), `feed`\n  (`/apps/communications/feed`), `mail`\n  (`/apps/communications/messages`) and `my_posts`\n  (`/apps/communications/my-posts`). There is **no `notifications`\n  item** — notifications are a platform surface the client owns and\n  reads from `GET /api/v1/notifications`, not a Communications tab.\n* **Training** returns a fixed native-app bottom-tab set: the learner\n  tabs `my_learning` (\"My Learning\", `/m/apps/training`),\n  `catalog` (\"Catalog\", `/m/apps/training/catalog`) and\n  `my_records` (\"My Records\", `/m/apps/training/certificates`)\n  are always present; `my_team` (\"My Team\",\n  `/m/apps/training/manager`) appears **only** for people-managers and\n  admins/HR admins. The `my_learning` title is TENANT-RENAMEABLE —\n  it is the Training app's `portal_title` setting, falling back to\n  \"My Learning\" when the tenant has not set one, so it always matches\n  the heading of the page the tab opens. Learning Paths is a course\n  *type* inside Catalog, not a tab, so it is not a navigation item.\n* **Recognitions** always returns `dashboard` (`/recognition`),\n  `feed` (`/recognition/feed`), `my_recognition`\n  (`/recognition/my_recognition`) and `leaderboard`\n  (`/recognition/leaderboard`), in that order. `programs`\n  (`/recognition/programs`) appears unless the business turned award\n  requests off (`enable_award_requests == false`); `awards`\n  (\"Award Cycles\", `/recognition/award-cycles`) appears **only** when\n  the business turned award cycles on (`enable_award_cycles == true`,\n  off by default); `team` (`/recognition/manager`) appears **only**\n  for recognition reviewers — business admins, Recognitions app\n  admins, and anyone with direct reports. The app's admin surfaces\n  (Analytics / Admin / Settings on the web rail) are deliberately not\n  part of this mobile navigation, and `team` carries no badge count.\n* **Company Store** returns the native-app bottom-tab set:\n  `dashboard` (\"Dashboard\", `/apps/company-store`), `catalog`\n  (\"Catalog\", `/apps/company-store/catalog`), `orders` (\"Orders\",\n  `/apps/company-store/orders`) and `balance` (\"Points\",\n  `/apps/company-store/balance`) are always present, in that order;\n  `approvals` (\"Approvals\",\n  `/apps/company-store/manager/approvals`) appears **only** for\n  redemption approvers — a member of the tenant's designated\n  redemption-approver group, or a manager with direct reports when no\n  such group is configured (a configured group supersedes the org\n  chart, so a line manager outside it is not an approver; admins with\n  direct reports are exempt from that supersession). A business admin\n  with **no** direct reports is not an approver here — admins act on\n  held orders through the admin orders queue instead. The rail's Cart\n  tab (which appears only once a cart exists), its Team Budget action\n  and its Admin / Settings dropdowns are not part of this mobile\n  navigation, and `approvals` carries no badge count.\n* **Frontline Execution** returns a native/mobile bottom bar curated\n  **per persona** (from the Frontline Execution prototype), not the\n  full set of tabs a caller may open. The caller is resolved to one of\n  three personas by role — `associate` (a plain member),\n  `manager` (a manager who is not an admin), and `head_office` (a\n  business admin/owner or the Frontline Execution app admin) — and\n  each gets a fixed, ordered bar:\n\n    * `associate` → `my_day` (\"My Day\",\n      `/apps/frontline-execution/my-day`), `requests` (\"Requests\",\n      `/apps/frontline-execution/requests`).\n    * `manager` → `my_day`, `requests`, `day_sheet` (\"Day Sheet\",\n      `/apps/frontline-execution/day-sheet`), `reviews` (\"Reviews\",\n      `/apps/frontline-execution/reviews`), `coverage` (\"Coverage\",\n      `/apps/frontline-execution/coverage`).\n    * `head_office` → `campaigns` (\"Campaigns\",\n      `/apps/frontline-execution/campaigns`), `coverage`, `requests`.\n\n  (The prototype's five bars collapse to three because the Store /\n  District / Regional manager tiers are all role=manager and are not\n  distinguishable from this payload.) Each item is then gated by its\n  tenant SURFACE TOGGLE, so a disabled surface never leaves a tab that\n  403s: `my_day` / `day_sheet` / `reviews` require `enable_my_day`,\n  `coverage` requires `enable_coverage`, and `requests` / `campaigns`\n  require `enable_campaigns`. Icons and labels follow the prototype\n  (`requests` = paper-plane, `coverage` = gauge). The web rail's\n  Overview, Request queue, Calendar, People, Shared blockers,\n  Analytics, Settings and Import surfaces are not part of this bar, and\n  no item carries a badge count.\n* **Safety Hub** returns the per-persona navigation from the Safety\n  Hub mobile prototype, in two zones. The **My** zone is every user's:\n  `safety_hub_my_submitted` (\"My Submitted\",\n  `/apps/safety-hub/submitted_by_me`) appears when either reporting\n  module (incidents / observations) is on; `safety_hub_my_alerts`\n  (\"My Alerts\", `/apps/safety-hub/alerts/my`) follows the\n  `emergency_alerts_enabled` toggle; `safety_hub_my_permits`\n  (\"My Permits\", `/apps/safety-hub/permits`, gated on\n  `permits_enabled`) and `safety_hub_my_corrective_actions`\n  (\"My Corrective Actions\", `/apps/safety-hub/corrective_actions`,\n  gated on `incidents_enabled`) appear **only for non-managers** (a\n  manager reaches the whole board from the Team zone, so emitting them\n  too would be a second row to the same path); `safety_hub_knowledge_base`\n  (\"Knowledge Base\", `/apps/safety-hub/knowledge_base`) is always\n  present. The **Team** zone appears **only** for managers — a\n  `manager_or_above?` user or the Safety Hub app admin — and holds\n  `safety_hub_team_alerts` (\"Team Alerts\", `/apps/safety-hub/alerts`),\n  `safety_hub_team_incidents` (\"Team Incidents\",\n  `/apps/safety-hub/incidents`), `safety_hub_team_observations`\n  (\"Team Observations\", `/apps/safety-hub/safety_observations`),\n  `safety_hub_team_permits` (\"Team Permits\", `/apps/safety-hub/permits`,\n  gated on `permits_enabled` — the whole permit board, the same index\n  My Permits narrows for a non-manager) and\n  `safety_hub_team_corrective_actions` (\"Team Corrective Actions\",\n  `/apps/safety-hub/corrective_actions`), each gated by its module\n  toggle. Keys are namespaced `safety_hub_*`; the app's compliance,\n  certifications, toolbox-talks, campaigns and admin surfaces are not\n  part of this navigation, and no item carries a badge count.\n\nGotchas:\n\n* An app can legitimately return `navigation_items: []` — either it\n  exposes no sub-navigation, or (on mobile) every tab it has was\n  dropped for lacking a real `/m/` route. Build the tile to open\n  `mobile_url` / `url` in that case.\n* Single-child collapse (mobile only): when an app would return\n  exactly ONE navigable item that belongs to its own mobile route,\n  the item is removed and `mobile_url` is repointed at it, so the\n  tile opens that page directly instead of a one-row submenu.\n* `navigation_items` is best-effort per app: if the underlying tab\n  builder raises, that app returns `[]` rather than failing the\n  request.\n",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "example": true
          }
        ],
        "responses": {
          "200": {
            "description": "Apps retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "apps": {
                      "type": "array",
                      "description": "Array of apps (both core and marketplace).\n\nWith `include_navigation=true` this holds only the\n**unpinned** enabled apps (the pinned ones move to\n`pinned_apps`), and every entry carries `pinned`,\n`has_mobile_view`, `mobile_url` and `navigation_items`.\n",
                      "items": {
                        "$ref": "#/components/schemas/ConsolidatedApp"
                      }
                    },
                    "pinned_apps": {
                      "type": "array",
                      "description": "The apps pinned for this user, in the same order the web\nsidebar pins them. **Only present when\n`include_navigation=true`.** Same item schema as `apps`,\nwith `pinned: true`.\n",
                      "items": {
                        "$ref": "#/components/schemas/ConsolidatedApp"
                      }
                    },
                    "pinned_count": {
                      "type": "integer",
                      "description": "Number of entries in `pinned_apps`. Only present when `include_navigation=true`.",
                      "example": 4
                    },
                    "apps_count": {
                      "type": "integer",
                      "description": "Number of entries in `apps` (unpinned). Only present when `include_navigation=true`.",
                      "example": 18
                    },
                    "total_count": {
                      "type": "integer",
                      "description": "Total number of apps returned. With\n`include_navigation=true` this is\n`pinned_count + apps_count`.\n",
                      "example": 12
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "core_count": {
                      "type": "integer",
                      "description": "Number of core apps in response",
                      "example": 6
                    },
                    "marketplace_count": {
                      "type": "integer",
                      "description": "Number of marketplace apps in response",
                      "example": 6
                    },
                    "enabled_count": {
                      "type": "integer",
                      "description": "Number of enabled apps",
                      "example": 9
                    },
                    "categories": {
                      "type": "array",
                      "description": "List of unique categories across all apps",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "HR Management",
                        "Workforce Management",
                        "Analytics"
                      ]
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                },
                "examples": {
                  "default": {
                    "summary": "All apps (default)",
                    "value": {
                      "apps": [
                        {
                          "id": "shifts_scheduling",
                          "slug": "shifts_scheduling",
                          "name": "Shifts & Scheduling",
                          "description": "Complete shift scheduling and management system",
                          "type": "core",
                          "category": "Workforce Management",
                          "enabled": true,
                          "icon": {
                            "url": "fas fa-calendar-alt",
                            "type": "icon_class"
                          },
                          "url": "/shifts",
                          "sort_order": 1
                        },
                        {
                          "id": 123,
                          "slug": "employee-performance-management",
                          "name": "Employee Performance Management",
                          "description": "Comprehensive performance management system",
                          "type": "marketplace",
                          "category": "performance",
                          "enabled": true,
                          "icon": {
                            "url": "fas fa-chart-line",
                            "type": "icon_class",
                            "web_url": "/assets/icons/employee-performance-management.svg"
                          },
                          "url": "/apps/employee-performance-management",
                          "sort_order": 10
                        }
                      ],
                      "total_count": 12,
                      "core_count": 6,
                      "marketplace_count": 6,
                      "enabled_count": 10,
                      "categories": [
                        "HR Management",
                        "Workforce Management",
                        "performance"
                      ]
                    }
                  },
                  "core_only": {
                    "summary": "Core apps only",
                    "value": {
                      "apps": [
                        {
                          "id": "shifts_scheduling",
                          "slug": "shifts_scheduling",
                          "name": "Shifts & Scheduling",
                          "type": "core",
                          "enabled": true,
                          "icon": {
                            "url": "fas fa-calendar-alt",
                            "type": "icon_class"
                          }
                        }
                      ],
                      "total_count": 6,
                      "core_count": 6,
                      "marketplace_count": 0,
                      "enabled_count": 6
                    }
                  },
                  "marketplace_only": {
                    "summary": "Marketplace apps only",
                    "value": {
                      "apps": [
                        {
                          "id": 123,
                          "slug": "employee-performance-management",
                          "name": "Employee Performance Management",
                          "type": "marketplace",
                          "enabled": true,
                          "icon": {
                            "url": "fas fa-chart-line",
                            "type": "icon_class",
                            "web_url": "/assets/icons/employee-performance-management.svg"
                          }
                        }
                      ],
                      "total_count": 6,
                      "core_count": 0,
                      "marketplace_count": 6,
                      "enabled_count": 4
                    }
                  },
                  "exclude_mobile_native": {
                    "summary": "Exclude mobile native apps",
                    "description": "Response when exclude_mobile_native=true, excluding shifts_scheduling, time_attendance, leave_management, timesheets",
                    "value": {
                      "apps": [
                        {
                          "id": "skills_certifications",
                          "slug": "skills_certifications",
                          "name": "Skills & Certifications",
                          "type": "core",
                          "category": "HR Management",
                          "enabled": true,
                          "icon": {
                            "url": "fas fa-graduation-cap",
                            "type": "icon_class"
                          }
                        },
                        {
                          "id": "compensation_management",
                          "slug": "compensation_management",
                          "name": "Compensation Management",
                          "type": "core",
                          "category": "HR Management",
                          "enabled": true,
                          "icon": {
                            "url": "fas fa-dollar-sign",
                            "type": "icon_class"
                          }
                        },
                        {
                          "id": 123,
                          "slug": "employee-performance-management",
                          "name": "Employee Performance Management",
                          "type": "marketplace",
                          "enabled": true,
                          "icon": {
                            "url": "fas fa-chart-line",
                            "type": "icon_class",
                            "web_url": "/assets/icons/employee-performance-management.svg"
                          }
                        }
                      ],
                      "total_count": 8,
                      "core_count": 2,
                      "marketplace_count": 6,
                      "enabled_count": 7,
                      "categories": [
                        "HR Management",
                        "performance"
                      ]
                    }
                  },
                  "include_navigation": {
                    "summary": "Grouped format with per-app navigation (include_navigation=true)",
                    "description": "Pinned/unpinned grouping with role-aware `navigation_items`.\nThis Ideas entry is what a NON-reviewer receives — the\n`review_queue` (\"Reviews\") item is absent because the caller\nbelongs to no review panel.\n",
                    "value": {
                      "pinned_apps": [
                        {
                          "id": 141,
                          "slug": "ideas",
                          "name": "Ideas",
                          "type": "marketplace",
                          "category": "Collaboration",
                          "enabled": true,
                          "pinned": true,
                          "icon": {
                            "url": "fas fa-lightbulb",
                            "type": "icon_class"
                          },
                          "url": "/apps/ideas",
                          "has_mobile_view": true,
                          "mobile_url": "/m/apps/ideas",
                          "navigation_items": [
                            {
                              "key": "dashboard",
                              "title": "Dashboard",
                              "icon": "fas fa-gauge",
                              "path": "/apps/ideas"
                            },
                            {
                              "key": "all_ideas",
                              "title": "Ideas",
                              "icon": "fas fa-lightbulb",
                              "path": "/apps/ideas/list"
                            },
                            {
                              "key": "campaigns",
                              "title": "Campaigns",
                              "icon": "fas fa-bullhorn",
                              "path": "/apps/ideas/campaigns"
                            }
                          ]
                        }
                      ],
                      "apps": [
                        {
                          "id": 152,
                          "slug": "wikis",
                          "name": "Wikis",
                          "type": "marketplace",
                          "category": "Knowledge",
                          "enabled": true,
                          "pinned": false,
                          "icon": {
                            "url": "fas fa-book",
                            "type": "icon_class"
                          },
                          "url": "/apps/wikis",
                          "has_mobile_view": true,
                          "mobile_url": "/m/apps/wikis",
                          "navigation_items": [
                            {
                              "key": "dashboard",
                              "title": "Dashboard",
                              "icon": "fas fa-gauge",
                              "path": "/apps/wikis"
                            },
                            {
                              "key": "all_wikis",
                              "title": "All wikis",
                              "icon": "fas fa-list-ul",
                              "path": "/apps/wikis/browse"
                            }
                          ]
                        },
                        {
                          "id": 168,
                          "slug": "safety-hub",
                          "name": "Safety Hub",
                          "type": "marketplace",
                          "category": "workplace-ops",
                          "enabled": true,
                          "pinned": false,
                          "icon": {
                            "url": "fas fa-shield-halved",
                            "type": "icon_class"
                          },
                          "url": "/apps/safety-hub",
                          "has_mobile_view": true,
                          "mobile_url": "/m/apps/safety-hub",
                          "navigation_items": [
                            {
                              "key": "safety_hub_my_submitted",
                              "title": "My Submitted",
                              "icon": "fas fa-clipboard-check",
                              "path": "/apps/safety-hub/submitted_by_me"
                            },
                            {
                              "key": "safety_hub_my_alerts",
                              "title": "My Alerts",
                              "icon": "fas fa-bullhorn",
                              "path": "/apps/safety-hub/alerts/my"
                            },
                            {
                              "key": "safety_hub_knowledge_base",
                              "title": "Knowledge Base",
                              "icon": "fas fa-book-open",
                              "path": "/apps/safety-hub/knowledge_base"
                            },
                            {
                              "key": "safety_hub_team_alerts",
                              "title": "Team Alerts",
                              "icon": "fas fa-bullhorn",
                              "path": "/apps/safety-hub/alerts"
                            },
                            {
                              "key": "safety_hub_team_incidents",
                              "title": "Team Incidents",
                              "icon": "fas fa-triangle-exclamation",
                              "path": "/apps/safety-hub/incidents"
                            },
                            {
                              "key": "safety_hub_team_observations",
                              "title": "Team Observations",
                              "icon": "fas fa-eye",
                              "path": "/apps/safety-hub/safety_observations"
                            },
                            {
                              "key": "safety_hub_team_permits",
                              "title": "Team Permits",
                              "icon": "far fa-file-lines",
                              "path": "/apps/safety-hub/permits"
                            },
                            {
                              "key": "safety_hub_team_corrective_actions",
                              "title": "Team Corrective Actions",
                              "icon": "fas fa-clipboard-check",
                              "path": "/apps/safety-hub/corrective_actions"
                            }
                          ]
                        }
                      ],
                      "total_count": 3,
                      "pinned_count": 1,
                      "apps_count": 2,
                      "categories": [
                        "Collaboration",
                        "Knowledge",
                        "workplace-ops"
                      ],
                      "unread_notification_count": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "Business context not available",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "No business context available"
                    },
                    "apps": {
                      "type": "array",
                      "items": {},
                      "example": []
                    },
                    "total_count": {
                      "type": "integer",
                      "example": 0
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/shifts": {
      "get": {
        "tags": [
          "Shifts"
        ],
        "summary": "List shifts",
        "description": "Returns shifts with flexible filtering and parameter-based querying. Supports:\n- User-specific shifts (user_id=current for authenticated user)\n- Open shifts available for claiming (type=open)\n- Current active shifts (type=current)\n- Period-based filtering (today, week, upcoming, past)\n- **NEW: Marketplace filters**\n  - `marketplace_available`: Shifts available in the marketplace that the user can claim\n  - `marketplace_claimed`: Shifts the user has claimed from the marketplace\n  - `marketplace_listed`: Shifts the user has listed in the marketplace\n",
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "description": "Type of shifts to retrieve.\n\n**Standard Types:**\n- `assigned`: User's assigned shifts (default)\n- `open`: Open shifts available for claiming\n- `current`: Currently active shifts\n- `all`: All shifts (admin/manager view)\n\n**Marketplace Types:**\n- `marketplace_available`: Shifts in marketplace available for claiming (excludes user's own shifts)\n- `marketplace_claimed`: Shifts user has claimed from marketplace (identified by marketplace_pickup flag)\n- `marketplace_listed`: Shifts user has listed for others to claim\n",
            "schema": {
              "type": "string",
              "enum": [
                "assigned",
                "open",
                "current",
                "all",
                "marketplace_available",
                "marketplace_claimed",
                "marketplace_listed"
              ],
              "default": "assigned"
            }
          },
          {
            "name": "period",
            "in": "query",
            "description": "Time period filter, evaluated against the shift's start/end time.\n\n`week` uses the TENANT'S configured week start (Settings → business\nhours), not a fixed Monday, so it matches what the web schedule shows.\n\nSupported with the default shift list, `type=assigned`,\n`type=marketplace_claimed` and `type=marketplace_listed`.\nCombining it with `type=open`, `type=current` or\n`type=marketplace_available` returns 400\n`period_status_filter_not_supported_for_type` — those views already\nreturn a fixed set (claimable upcoming shifts, or the shifts in\nprogress right now) and silently ignoring the filter would ship a\nlist the caller believes was narrowed.\n",
            "schema": {
              "type": "string",
              "enum": [
                "current",
                "today",
                "week",
                "upcoming",
                "past"
              ]
            }
          },
          {
            "name": "user_id",
            "in": "query",
            "description": "User ID or 'current' for authenticated user's shifts",
            "schema": {
              "oneOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "string",
                  "enum": [
                    "current"
                  ]
                }
              ]
            }
          },
          {
            "name": "location_id",
            "in": "query",
            "description": "Filter by specific location ID. For multiple locations, use location_ids[] instead.\nAlso accepts comma-separated values (e.g., location_id=1,2,3) for backwards compatibility.\n",
            "schema": {
              "type": "string"
            },
            "example": "1"
          },
          {
            "name": "location_ids[]",
            "in": "query",
            "description": "Filter by multiple location IDs. Use this to fetch shifts across all locations \nassigned to a user. Can be passed as an array (location_ids[]=1&location_ids[]=2) \nor comma-separated (location_ids=1,2,3).\n",
            "schema": {
              "type": "array",
              "items": {
                "type": "integer"
              }
            },
            "style": "form",
            "explode": true,
            "example": [
              1,
              2,
              3
            ]
          },
          {
            "name": "scope",
            "in": "query",
            "description": "Narrow the list to shifts belonging to the CALLING user's own\nlocations, departments, or teams. Takes no ids — the scope is\nresolved server-side from the authenticated user, so a client never\nhas to hold a synced copy of their memberships.\n\nA shift is in scope when EITHER\n  (a) the shift carries one of the USER'S OWN ids for the dimension\n      — it is at one of their locations, or it (or its SCHEDULE) is\n      for one of their teams / departments, OR\n  (b) the shift carries THAT DIMENSION AT ALL, and somebody\n      assigned to it is one of the user's people.\n\nBoth halves matter: (a) alone misses a shift your employee picked\nup at another site; (b) alone misses an UNASSIGNED shift at your\nown site, which has nobody on it to make it yours.\n\nThe dimension gate on (b) is what makes the scope name mean what it\nsays. `scope=team` returns TEAM shifts only: a shift grouped by\nLOCATION, or an ad-hoc shift with no grouping at all, is not\nreturned just because a teammate happens to be rostered on it.\nLikewise `scope=department` returns only shifts that carry a\ndepartment. The gate is a no-op for `scope=location`, where no\nshift can lack a location.\n\n`scope=team` means CUSTOM scheduling teams. The three group types\neach answer to their own scope name: `location` groups (auto-created\nper site) to `scope=location`, `department` groups (auto-created\nfrom an org-chart department) to `scope=department`, and `custom`\ngroups to `scope=team`. Note a team built by hand and filtered to a\ndepartment is stored as `custom`, so it answers to `scope=team`.\n\nComposes with `location_id` / `start_date` etc. rather than\nreplacing them — every filter narrows. A user whose scope resolves\nto nothing receives an empty list, never the whole business.\n\nAVAILABLE TO EVERY ROLE, INCLUDING A PLAIN EMPLOYEE (2026-09-02).\nOn the DEFAULT list a caller who is not entitled to a business-wide\nread normally receives only their own assigned shifts; passing a\nresolved `scope` lifts that to their own unit — their colleagues'\nshifts, plus unassigned shifts at the unit, plus a colleague's shift\nworked at another site (half (b)). This is parity with the web Team\nCalendar, which has served an employee the same roster since\n2026-07-09.\n\nThe reach is the UNIT and never the tenant: the resolver bounds an\nemployee's `location` dimension to their own assigned locations, and\n`team` / `department` to memberships they actually hold.\n\nTWO THINGS IT DOES NOT LIFT.\n  * A token carrying only `read:own_shifts` is unaffected — it still\n    receives own-assignment rows with or without `scope`. The\n    widening requires `read:shifts` / `write:shifts` / `admin`, or a\n    session, exactly as a business-wide read does.\n  * `user_id=<someone else>` is still refused with 403. Reading your\n    unit's week is not the same as reading one named colleague's\n    schedule.\n\nTeam and department are read from the shift's SCHEDULE as well as\nthe shift's own columns, because tenants attach them to the\nschedule and only some of it is copied down. `scope=department`\nadditionally matches department-flavoured scheduling groups, which\nis the only way an ORG-CHART department resolves at all — neither\nshifts nor schedules carry a column for one.\n\nSupported with the DEFAULT shift list and `type=open` only.\nCombining it with `type=assigned|current|marketplace_*`, or with\n`user_id=current`, returns 400 `scope_not_supported_for_type` —\nthose views are already narrowed to the caller, and silently\nignoring the filter would return a response whose\n`meta.user_scope` named a scope that was never applied.\n\nResponses include `meta.user_scope` describing what the scope\nresolved to. A caller that resolves to no locations, teams or\ndepartments receives an EMPTY list — the filter fails closed.\n",
            "schema": {
              "type": "string",
              "enum": [
                "location",
                "department",
                "team"
              ]
            },
            "example": "location"
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by shift status.\n\nSupported with the same types as `period` (see above); `type=open`,\n`type=current` and `type=marketplace_available` return 400\n`period_status_filter_not_supported_for_type`. On\n`type=marketplace_listed` this filters the LISTING status, not the\nshift status.\n\nNOTE: `open` is NOT a valid shift status — the model permits only\nscheduled / completed / cancelled / late_reported / absence_reported,\nso `?status=open` matches zero rows. Use `?type=open` to list shifts\navailable for claiming.\n",
            "schema": {
              "type": "string",
              "enum": [
                "scheduled",
                "completed",
                "cancelled",
                "late_reported",
                "absence_reported"
              ]
            }
          },
          {
            "name": "date",
            "in": "query",
            "description": "Filter shifts by specific date (YYYY-MM-DD)",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "start_date",
            "in": "query",
            "description": "Lower bound on the shift's START date (inclusive).\n",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "end_date",
            "in": "query",
            "description": "Upper bound on the shift's START date (inclusive) — i.e. `start_date`\nand `end_date` together select the shifts that BEGIN inside the\nwindow, which is what the web date filter does.\n\nThis bound was previously applied to `end_time`, which silently\nexcluded every open-ended (ad-hoc) shift — those have no end time —\nand every overnight shift that began inside the window but finished\nafter it.\n",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "include",
            "in": "query",
            "description": "Additional data to include. Accepts a comma-separated string\n(`include=a,b`) or repeated bracket notation (`include[]=a&include[]=b`).\n\n`user_scope` adds a `meta.user_scope` object listing the caller's\nown locations, departments and teams. It is included automatically\nwhenever `scope` is passed; request it explicitly to read the\nmemberships WITHOUT filtering by them.\n\n`user_scope.departments` names the caller's DEPARTMENT TEAMS —\nscheduling groups of `group_type: \"department\"`, each rendered as\n`{ id, name, type: \"department_team\", group_type,\norganizational_department_id }`. These are the same objects a\nmatched shift echoes in its own `scheduling_group`, so a client can\nreconcile the two by id. Use `organizational_department_id` to reach\nthe org-chart department the team was derived from.\n\nCAVEAT: `scope=department` ALSO matches shifts on\n`shifts.location_department_id` / `schedule_locations.location_department_id`,\nand those matches have no entry in `user_scope.departments` — a\nshift can therefore be returned with no meta row explaining it, and\na caller whose only department signal is a LocationDepartment sees\n`departments: []` alongside a non-empty shift list. Prior to\n2026-08-27 this array carried LocationDepartment and\nOrganizationalDepartment records instead; it was changed so the meta\nnames the same objects the items do.\n\nCosts ~13 extra queries\n(measured), and is paid on EVERY page, so it is off by default for\nclients that page this endpoint in a loop.\n\n`scope_users` adds `meta.user_scope.user_ids` — the people the scope\nresolved to, i.e. the users who share the requested dimension with\nthe caller. It answers \"who are my colleagues for this scope\", which\nis NOT the same question as `teammates`:\n\n  teammates    the people assigned to each returned shift\n  user_ids     everyone in my location / team / department, whether\n               or not they have a shift in this response\n\nSo a shift can be returned for a person who is NOT in `user_ids`\n(it is at your location, but they are not one of your people), and\n`user_ids` can name someone with no shift in the response at all.\n\nCosts no extra queries on a `?scope=`d request — the filter already\nresolved that list to build its roster half.\n\nIDS ONLY, deliberately: hydrating names and avatars costs roughly\ntwo queries per user, so fetch the people themselves from\n`/api/v1/users` when you need more than an id.\n\nThe list names the caller's colleagues when the RESPONSE ROWS are\nroster-wide, and is bounded to the caller's own id otherwise\n(`counts.users` then 1, or 0 when the scope resolved to nothing).\nRows are roster-wide for a business-wide read, and — since\n2026-09-02 — for any role that passed a resolved `scope` on a\ncredential permitted to read shifts.\n\nSo the bound still applies to:\n  * a caller on the DEFAULT list with no `scope`, whose rows are\n    their own assignments, and\n  * a caller of ANY role, manager and admin included, whose token\n    carries only `read:own_shifts`.\n\nUnchanged property: the bound never depends on `type`. One resolver\nis memoized per request and shared by every handler, so `user_ids`\nis a function of the caller's entitlement and of whether a `scope`\nwas passed — not of which `type` view ran. That matters for\n`type=open`, whose items are unassigned shifts rather than the\ncaller's own.\n\nIT IS THE ROSTER, NOT AN INDEX OF ASSIGNEES. The list is \"my\npeople\" — the members of the resolved unit. A shift returned by\nhalf (a) may be worked by somebody who does NOT belong to that unit\n(an outsider covering a shift at your site); their shift is in\n`items` and their id is deliberately NOT here. Read an assignee from\nthe shift row or `include=teammates`, never by assuming this list\ncovers every person you will encounter.\n\nCapped at 50 like the other `user_scope` arrays, with\n`counts.users` always EXACT and the shared `truncated` flag set when\nthe array was cut. For an ADMIN on `scope=location` the list is\ntenant-sized, because an admin's location scope is every location in\nthe business — read `counts.users` rather than the array length.\nFor `scope=team` and `scope=department` it is membership-only and\nnever role-widened. With no `scope`, `user_ids` is empty by\ndefinition.\n\n`attendance` REQUIRES `format=detailed` ON THIS ENDPOINT — verified\nover HTTP 2026-09-02. Each item's `attendance_records` array is\nemitted only by the detailed serializer, and this list defaults to\n`format=standard`, so `include=attendance` on its own answers 200\nwith no attendance data and nothing in the response to say why —\nnote the example value below includes `attendance`, so following it\nliterally is what surfaces this. Pass\n`format=detailed&include=attendance`. `GET /api/v1/shifts/{id}`\nneeds no such pairing — its `format` already defaults to\n`detailed`. `teammates` and `notes` are unaffected: they render at\nevery format.\n",
            "schema": {
              "type": "string",
              "example": "teammates,notes,attendance,user_scope,scope_users"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "Response detail level",
            "schema": {
              "type": "string",
              "enum": [
                "minimal",
                "standard",
                "detailed"
              ],
              "default": "standard"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of shifts",
            "headers": {
              "X-Total-Count": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Total-Pages": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Current-Page": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Per-Page": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 123
                          },
                          "name": {
                            "type": "string",
                            "example": "Morning Shift"
                          },
                          "description": {
                            "type": "string",
                            "nullable": true
                          },
                          "start_time": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2025-01-27T09:00:00Z"
                          },
                          "end_time": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2025-01-27T17:00:00Z"
                          },
                          "formatted_date": {
                            "type": "string",
                            "description": "Human-readable date format",
                            "example": "Monday, Jan 27"
                          },
                          "formatted_time": {
                            "type": "string",
                            "description": "Human-readable time range",
                            "example": "9:00 AM - 5:00 PM"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "scheduled",
                              "completed",
                              "cancelled",
                              "open"
                            ],
                            "example": "scheduled"
                          },
                          "urgent": {
                            "type": "boolean",
                            "description": "Whether this shift is marked as urgent",
                            "example": false
                          },
                          "needs_coverage": {
                            "type": "boolean",
                            "description": "Whether this shift needs coverage",
                            "example": false
                          },
                          "can_list_for_coverage": {
                            "type": "boolean",
                            "description": "Whether the current user can list this shift for coverage in the marketplace",
                            "example": true
                          },
                          "is_already_listed": {
                            "type": "boolean",
                            "description": "Whether this shift is already listed in the marketplace by the current user",
                            "example": false
                          },
                          "required_users": {
                            "type": "integer",
                            "description": "Number of users required for this shift"
                          },
                          "location": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 1
                              },
                              "name": {
                                "type": "string",
                                "example": "Downtown Store"
                              },
                              "address": {
                                "type": "string",
                                "nullable": true
                              },
                              "phone": {
                                "type": "string",
                                "nullable": true
                              }
                            }
                          },
                          "users": {
                            "type": "array",
                            "items": {
                              "$ref": "#/components/schemas/User"
                            }
                          },
                          "available_actions": {
                            "type": "array",
                            "description": "Actions available to the current user for this shift",
                            "items": {
                              "type": "string",
                              "enum": [
                                "view_details",
                                "request_coverage",
                                "report_lateness",
                                "view_teammates",
                                "claim"
                              ]
                            },
                            "example": [
                              "view_details",
                              "request_coverage",
                              "view_teammates"
                            ]
                          },
                          "teammates": {
                            "type": "array",
                            "description": "Other users assigned to this shift (when include=teammates)",
                            "nullable": true,
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "type": "integer",
                                  "description": "User ID"
                                },
                                "name": {
                                  "type": "string",
                                  "description": "User full name"
                                },
                                "job_title": {
                                  "type": "string",
                                  "nullable": true,
                                  "description": "User's job title/role"
                                },
                                "profile_photo_url": {
                                  "type": "string",
                                  "nullable": true,
                                  "description": "Full-size profile photo URL (200x200) or avatar placeholder"
                                },
                                "profile_photo_thumbnail_url": {
                                  "type": "string",
                                  "nullable": true,
                                  "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                                }
                              }
                            }
                          },
                          "listed_by": {
                            "type": "object",
                            "nullable": true,
                            "description": "User who listed this shift in the marketplace (teammate format, present for marketplace shifts)",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "description": "User ID"
                              },
                              "name": {
                                "type": "string",
                                "description": "User full name"
                              },
                              "job_title": {
                                "type": "string",
                                "nullable": true,
                                "description": "User's job title/role"
                              },
                              "profile_photo_url": {
                                "type": "string",
                                "nullable": true,
                                "description": "Full-size profile photo URL (200x200) or avatar placeholder"
                              },
                              "profile_photo_thumbnail_url": {
                                "type": "string",
                                "nullable": true,
                                "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                              }
                            }
                          },
                          "picked_by": {
                            "type": "object",
                            "nullable": true,
                            "description": "User who claimed/picked up this shift from the marketplace (teammate format, present when shift was claimed)",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "description": "User ID"
                              },
                              "name": {
                                "type": "string",
                                "description": "User full name"
                              },
                              "job_title": {
                                "type": "string",
                                "nullable": true,
                                "description": "User's job title/role"
                              },
                              "profile_photo_url": {
                                "type": "string",
                                "nullable": true,
                                "description": "Full-size profile photo URL (200x200) or avatar placeholder"
                              },
                              "profile_photo_thumbnail_url": {
                                "type": "string",
                                "nullable": true,
                                "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                              }
                            }
                          },
                          "marketplace_info": {
                            "type": "object",
                            "nullable": true,
                            "description": "Marketplace listing information (present when shift has marketplace listing)",
                            "properties": {
                              "has_listing": {
                                "type": "boolean",
                                "description": "Whether this shift has an active marketplace listing",
                                "example": true
                              },
                              "listing_id": {
                                "type": "integer",
                                "description": "ID of the marketplace listing",
                                "example": 880
                              },
                              "listing_status": {
                                "type": "string",
                                "enum": [
                                  "open",
                                  "filled",
                                  "closed",
                                  "cancelled"
                                ],
                                "description": "Current status of the marketplace listing",
                                "example": "open"
                              },
                              "listing_type": {
                                "type": "string",
                                "enum": [
                                  "pickup",
                                  "trade_only",
                                  "both"
                                ],
                                "description": "Type of marketplace listing - pickup (direct claim), trade_only (requires application/trade), or both (accepts either)",
                                "example": "pickup"
                              },
                              "price": {
                                "type": "number",
                                "format": "float",
                                "description": "Price offered for the shift",
                                "example": 0.01
                              },
                              "currency": {
                                "type": "string",
                                "description": "Currency code (e.g., USD)",
                                "example": "USD"
                              },
                              "listed_by": {
                                "type": "integer",
                                "description": "User ID of the person who listed the shift (legacy field, use listed_by object instead)",
                                "example": 1487
                              },
                              "listed_at": {
                                "type": "string",
                                "format": "date-time",
                                "description": "When the shift was listed in the marketplace",
                                "example": "2025-12-01T10:05:00Z"
                              }
                            }
                          },
                          "notes": {
                            "type": "string",
                            "nullable": true,
                            "description": "Shift notes (when include=notes)"
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "meta": {
                      "allOf": [
                        {
                          "type": "object",
                          "description": "Pagination metadata",
                          "properties": {
                            "total_count": {
                              "type": "integer",
                              "description": "Total number of items",
                              "example": 150
                            },
                            "total_pages": {
                              "type": "integer",
                              "description": "Total number of pages",
                              "example": 6
                            },
                            "current_page": {
                              "type": "integer",
                              "description": "Current page number",
                              "example": 1
                            },
                            "per_page": {
                              "type": "integer",
                              "description": "Items per page",
                              "example": 25
                            }
                          },
                          "required": [
                            "total_count",
                            "total_pages",
                            "current_page",
                            "per_page"
                          ]
                        },
                        {
                          "type": "object",
                          "properties": {
                            "current_shift": {
                              "$ref": "../openapi.yaml#/components/schemas/Shift",
                              "nullable": true
                            },
                            "user_timezone": {
                              "type": "string",
                              "example": "America/New_York"
                            },
                            "type": {
                              "type": "string",
                              "example": "open_shifts"
                            }
                          }
                        }
                      ]
                    }
                  }
                },
                "examples": {
                  "my_shifts": {
                    "summary": "My assigned shifts",
                    "value": {
                      "items": [
                        {
                          "id": 123,
                          "name": "Morning Shift",
                          "date": "Monday, Jan 27",
                          "start_time": "2025-01-27T09:00:00Z",
                          "end_time": "2025-01-27T17:00:00Z",
                          "location": {
                            "id": 1,
                            "name": "Downtown Store"
                          },
                          "status": "scheduled",
                          "urgent": false,
                          "needs_coverage": false,
                          "available_actions": [
                            "view_details",
                            "request_coverage",
                            "view_teammates"
                          ]
                        }
                      ],
                      "meta": {
                        "total_count": 15,
                        "current_page": 1,
                        "total_pages": 2,
                        "per_page": 25,
                        "current_shift": null,
                        "user_timezone": "America/New_York"
                      }
                    }
                  },
                  "open_shifts": {
                    "summary": "Open shifts available for claiming",
                    "value": {
                      "items": [
                        {
                          "id": 456,
                          "name": "Evening Shift",
                          "date": "Tuesday, Jan 28",
                          "start_time": "2025-01-28T14:00:00Z",
                          "end_time": "2025-01-28T22:00:00Z",
                          "location": {
                            "id": 2,
                            "name": "Mall Location"
                          },
                          "status": "open",
                          "urgent": true,
                          "needs_coverage": true,
                          "available_actions": [
                            "claim",
                            "view_details"
                          ]
                        }
                      ],
                      "meta": {
                        "total_count": 5,
                        "type": "open_shifts"
                      }
                    }
                  },
                  "marketplace_available": {
                    "summary": "Marketplace - Available shifts to claim",
                    "value": {
                      "items": [
                        {
                          "id": 789,
                          "name": "Weekend Shift",
                          "date": "Saturday, Feb 1",
                          "start_time": "2025-02-01T10:00:00Z",
                          "end_time": "2025-02-01T18:00:00Z",
                          "formatted_date": "Saturday, Feb 01",
                          "formatted_time": "10:00 AM - 06:00 PM",
                          "location": {
                            "id": 3,
                            "name": "North Branch"
                          },
                          "status": "scheduled",
                          "urgent": false,
                          "needs_coverage": true,
                          "available_actions": [
                            "claim",
                            "view_details"
                          ],
                          "marketplace_info": {
                            "has_listing": true,
                            "listing_id": 456,
                            "price": 50.0,
                            "currency": "USD",
                            "listed_by": 123,
                            "listed_at": "2025-01-15T10:00:00Z"
                          },
                          "listed_by": {
                            "id": 123,
                            "name": "Grace Kulin",
                            "job_title": "Product Manager",
                            "profile_photo_url": "https://hub.workforce.mangoapps.com/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6NTk1LCJwdXIiOiJibG9iX2lkIn19--3c6dbf568dc4bf5029665ab667828e9ef1ddb58e/IMG_0130.jpg"
                          }
                        }
                      ],
                      "meta": {
                        "total_count": 8,
                        "current_page": 1,
                        "total_pages": 1,
                        "per_page": 25,
                        "type": "marketplace_available"
                      }
                    }
                  },
                  "marketplace_claimed": {
                    "summary": "Marketplace - Shifts I've claimed",
                    "value": {
                      "items": [
                        {
                          "id": 890,
                          "name": "Evening Shift",
                          "date": "Thursday, Jan 30",
                          "start_time": "2025-01-30T15:00:00Z",
                          "end_time": "2025-01-30T23:00:00Z",
                          "formatted_date": "Thursday, Jan 30",
                          "formatted_time": "03:00 PM - 11:00 PM",
                          "location": {
                            "id": 4,
                            "name": "South Branch"
                          },
                          "status": "scheduled",
                          "urgent": false,
                          "needs_coverage": false,
                          "available_actions": [
                            "view_details",
                            "request_coverage"
                          ],
                          "marketplace_info": {
                            "has_listing": true,
                            "listing_id": 457,
                            "price": 45.0,
                            "currency": "USD",
                            "listed_by": 456,
                            "listed_at": "2025-01-20T14:00:00Z"
                          },
                          "listed_by": {
                            "id": 456,
                            "name": "John Smith",
                            "job_title": "Shift Supervisor",
                            "profile_photo_url": "https://hub.workforce.mangoapps.com/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6NTk1LCJwdXIiOiJibG9iX2lkIn19--3c6dbf568dc4bf5029665ab667828e9ef1ddb58e/profile.jpg"
                          },
                          "picked_by": {
                            "id": 1256,
                            "name": "Grace Kulin",
                            "job_title": "Product Manager",
                            "profile_photo_url": "https://hub.workforce.mangoapps.com/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6NTk1LCJwdXIiOiJibG9iX2lkIn19--3c6dbf568dc4bf5029665ab667828e9ef1ddb58e/IMG_0130.jpg"
                          }
                        }
                      ],
                      "meta": {
                        "total_count": 3,
                        "current_page": 1,
                        "total_pages": 1,
                        "per_page": 25,
                        "type": "marketplace_claimed"
                      }
                    }
                  },
                  "marketplace_listed": {
                    "summary": "Marketplace - Shifts I've listed",
                    "value": {
                      "items": [
                        {
                          "id": 901,
                          "name": "Late Night Shift",
                          "date": "Friday, Feb 7",
                          "start_time": "2025-02-07T22:00:00Z",
                          "end_time": "2025-02-08T06:00:00Z",
                          "formatted_date": "Friday, Feb 07",
                          "formatted_time": "10:00 PM - 06:00 AM",
                          "location": {
                            "id": 1,
                            "name": "Downtown Store"
                          },
                          "status": "scheduled",
                          "urgent": false,
                          "needs_coverage": true,
                          "available_actions": [
                            "view_details"
                          ],
                          "marketplace_info": {
                            "has_listing": true,
                            "listing_id": 458,
                            "price": 75.0,
                            "currency": "USD",
                            "listed_by": 789,
                            "listed_at": "2025-01-25T09:00:00Z"
                          },
                          "listed_by": {
                            "id": 789,
                            "name": "Sarah Johnson",
                            "job_title": "Operations Manager",
                            "profile_photo_url": "https://hub.workforce.mangoapps.com/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6NTk1LCJwdXIiOiJibG9iX2lkIn19--3c6dbf568dc4bf5029665ab667828e9ef1ddb58e/sarah.jpg"
                          }
                        }
                      ],
                      "meta": {
                        "total_count": 2,
                        "current_page": 1,
                        "total_pages": 1,
                        "per_page": 25,
                        "type": "marketplace_listed"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/shifts/{id}": {
      "get": {
        "tags": [
          "Shifts"
        ],
        "summary": "Get shift details",
        "description": "Retrieves detailed information about a specific shift, including:\n- Basic shift information (name, times, location, status)\n- Detailed location information\n- Shift details (description, notes, staffing counts)\n- **Marketplace listing information** (when available)\n  - `listing_type`: Type of listing (pickup, trade_only, both)\n  - `listing_status`: Current status (open, filled, closed, cancelled)\n  - Price, currency, and listing metadata\n- Available actions for the current user\n- Assignment capabilities\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Shift ID",
            "schema": {
              "type": "integer",
              "example": 3901
            }
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "Response format type",
            "schema": {
              "type": "string",
              "enum": [
                "minimal",
                "standard",
                "detailed"
              ],
              "default": "detailed"
            }
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "description": "Additional fields to include (comma-separated)",
            "schema": {
              "type": "string",
              "example": "teammates,notes,attendance"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Shift details retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 123
                        },
                        "name": {
                          "type": "string",
                          "example": "Morning Shift"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true
                        },
                        "start_time": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2025-01-27T09:00:00Z"
                        },
                        "end_time": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2025-01-27T17:00:00Z"
                        },
                        "formatted_date": {
                          "type": "string",
                          "description": "Human-readable date format",
                          "example": "Monday, Jan 27"
                        },
                        "formatted_time": {
                          "type": "string",
                          "description": "Human-readable time range",
                          "example": "9:00 AM - 5:00 PM"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "scheduled",
                            "completed",
                            "cancelled",
                            "open"
                          ],
                          "example": "scheduled"
                        },
                        "urgent": {
                          "type": "boolean",
                          "description": "Whether this shift is marked as urgent",
                          "example": false
                        },
                        "needs_coverage": {
                          "type": "boolean",
                          "description": "Whether this shift needs coverage",
                          "example": false
                        },
                        "can_list_for_coverage": {
                          "type": "boolean",
                          "description": "Whether the current user can list this shift for coverage in the marketplace",
                          "example": true
                        },
                        "is_already_listed": {
                          "type": "boolean",
                          "description": "Whether this shift is already listed in the marketplace by the current user",
                          "example": false
                        },
                        "required_users": {
                          "type": "integer",
                          "description": "Number of users required for this shift"
                        },
                        "location": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 1
                            },
                            "name": {
                              "type": "string",
                              "example": "Downtown Store"
                            },
                            "address": {
                              "type": "string",
                              "nullable": true
                            },
                            "phone": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "users": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/User"
                          }
                        },
                        "available_actions": {
                          "type": "array",
                          "description": "Actions available to the current user for this shift",
                          "items": {
                            "type": "string",
                            "enum": [
                              "view_details",
                              "request_coverage",
                              "report_lateness",
                              "view_teammates",
                              "claim"
                            ]
                          },
                          "example": [
                            "view_details",
                            "request_coverage",
                            "view_teammates"
                          ]
                        },
                        "teammates": {
                          "type": "array",
                          "description": "Other users assigned to this shift (when include=teammates)",
                          "nullable": true,
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "description": "User ID"
                              },
                              "name": {
                                "type": "string",
                                "description": "User full name"
                              },
                              "job_title": {
                                "type": "string",
                                "nullable": true,
                                "description": "User's job title/role"
                              },
                              "profile_photo_url": {
                                "type": "string",
                                "nullable": true,
                                "description": "Full-size profile photo URL (200x200) or avatar placeholder"
                              },
                              "profile_photo_thumbnail_url": {
                                "type": "string",
                                "nullable": true,
                                "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                              }
                            }
                          }
                        },
                        "listed_by": {
                          "type": "object",
                          "nullable": true,
                          "description": "User who listed this shift in the marketplace (teammate format, present for marketplace shifts)",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "description": "User ID"
                            },
                            "name": {
                              "type": "string",
                              "description": "User full name"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true,
                              "description": "User's job title/role"
                            },
                            "profile_photo_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200) or avatar placeholder"
                            },
                            "profile_photo_thumbnail_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                            }
                          }
                        },
                        "picked_by": {
                          "type": "object",
                          "nullable": true,
                          "description": "User who claimed/picked up this shift from the marketplace (teammate format, present when shift was claimed)",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "description": "User ID"
                            },
                            "name": {
                              "type": "string",
                              "description": "User full name"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true,
                              "description": "User's job title/role"
                            },
                            "profile_photo_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200) or avatar placeholder"
                            },
                            "profile_photo_thumbnail_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                            }
                          }
                        },
                        "marketplace_info": {
                          "type": "object",
                          "nullable": true,
                          "description": "Marketplace listing information (present when shift has marketplace listing)",
                          "properties": {
                            "has_listing": {
                              "type": "boolean",
                              "description": "Whether this shift has an active marketplace listing",
                              "example": true
                            },
                            "listing_id": {
                              "type": "integer",
                              "description": "ID of the marketplace listing",
                              "example": 880
                            },
                            "listing_status": {
                              "type": "string",
                              "enum": [
                                "open",
                                "filled",
                                "closed",
                                "cancelled"
                              ],
                              "description": "Current status of the marketplace listing",
                              "example": "open"
                            },
                            "listing_type": {
                              "type": "string",
                              "enum": [
                                "pickup",
                                "trade_only",
                                "both"
                              ],
                              "description": "Type of marketplace listing - pickup (direct claim), trade_only (requires application/trade), or both (accepts either)",
                              "example": "pickup"
                            },
                            "price": {
                              "type": "number",
                              "format": "float",
                              "description": "Price offered for the shift",
                              "example": 0.01
                            },
                            "currency": {
                              "type": "string",
                              "description": "Currency code (e.g., USD)",
                              "example": "USD"
                            },
                            "listed_by": {
                              "type": "integer",
                              "description": "User ID of the person who listed the shift (legacy field, use listed_by object instead)",
                              "example": 1487
                            },
                            "listed_at": {
                              "type": "string",
                              "format": "date-time",
                              "description": "When the shift was listed in the marketplace",
                              "example": "2025-12-01T10:05:00Z"
                            }
                          }
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true,
                          "description": "Shift notes (when include=notes)"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    {
                      "type": "object",
                      "properties": {
                        "can_list_for_coverage": {
                          "type": "boolean",
                          "description": "Whether the current user can list this shift for coverage",
                          "example": true
                        },
                        "is_already_listed": {
                          "type": "boolean",
                          "description": "Whether this shift is already listed by the current user",
                          "example": false
                        }
                      }
                    }
                  ]
                },
                "examples": {
                  "shift_with_marketplace_listing": {
                    "summary": "Shift with marketplace listing",
                    "value": {
                      "id": 3901,
                      "name": "Full Day Coverage - Pune Office",
                      "start_time": "2025-12-06T04:30:00Z",
                      "end_time": "2025-12-06T13:00:00Z",
                      "formatted_date": "Friday, Dec 06",
                      "formatted_time": "04:30 AM - 01:00 PM",
                      "location": {
                        "id": 42,
                        "name": "Pune Office"
                      },
                      "status": "scheduled",
                      "urgent": false,
                      "needs_coverage": false,
                      "available_actions": [
                        "view_details",
                        "request_coverage"
                      ],
                      "marketplace_info": {
                        "has_listing": true,
                        "listing_id": 880,
                        "listing_status": "open",
                        "listing_type": "pickup",
                        "price": "0.01",
                        "currency": "USD",
                        "listed_by": 1487,
                        "listed_at": "2025-12-01T10:05:00Z"
                      },
                      "listed_by": {
                        "id": 1487,
                        "name": "John Doe",
                        "job_title": "Operations Manager",
                        "profile_photo_url": "https://hub.workforce.mangoapps.com/avatar/1487"
                      },
                      "location_details": {
                        "id": 42,
                        "name": "Pune Office",
                        "address": "123 Tech Park, Pune",
                        "phone": "+91-20-12345678"
                      },
                      "shift_details": {
                        "description": "Regular operations shift",
                        "notes": "Remember to bring your access card",
                        "required_staff_count": 5,
                        "current_staff_count": 4
                      },
                      "can_list_for_coverage": false,
                      "is_already_listed": true
                    }
                  },
                  "shift_without_marketplace": {
                    "summary": "Shift without marketplace listing",
                    "value": {
                      "id": 767,
                      "name": "Operations - Early Shift",
                      "start_time": "2025-09-23T14:00:00Z",
                      "end_time": "2025-09-23T22:30:00Z",
                      "formatted_date": "Tuesday, Sep 23",
                      "formatted_time": "02:00 PM - 10:30 PM",
                      "location": {
                        "id": 265,
                        "name": "Issaquah Office"
                      },
                      "status": "scheduled",
                      "urgent": false,
                      "needs_coverage": false,
                      "available_actions": [
                        "view_details"
                      ],
                      "location_details": {
                        "id": 265,
                        "name": "Issaquah Office",
                        "address": "1495 11th Avenue Northwest",
                        "phone": ""
                      },
                      "shift_details": {
                        "description": "",
                        "notes": null,
                        "required_staff_count": 4,
                        "current_staff_count": 4
                      },
                      "can_list_for_coverage": false,
                      "is_already_listed": false
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not authorized to access this shift",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                },
                "example": {
                  "error": "Not authorized to access this shift"
                }
              }
            }
          },
          "404": {
            "description": "Shift not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                },
                "example": {
                  "error": {
                    "code": "shift_not_found",
                    "message": "Shift not found"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/shifts/{id}/claim": {
      "post": {
        "tags": [
          "Shifts"
        ],
        "summary": "Claim an open shift",
        "description": "Employee claims an available open shift",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Shift claimed successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Shift claimed successfully"
                    },
                    "shift": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 123
                        },
                        "name": {
                          "type": "string",
                          "example": "Morning Shift"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true
                        },
                        "start_time": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2025-01-27T09:00:00Z"
                        },
                        "end_time": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2025-01-27T17:00:00Z"
                        },
                        "formatted_date": {
                          "type": "string",
                          "description": "Human-readable date format",
                          "example": "Monday, Jan 27"
                        },
                        "formatted_time": {
                          "type": "string",
                          "description": "Human-readable time range",
                          "example": "9:00 AM - 5:00 PM"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "scheduled",
                            "completed",
                            "cancelled",
                            "open"
                          ],
                          "example": "scheduled"
                        },
                        "urgent": {
                          "type": "boolean",
                          "description": "Whether this shift is marked as urgent",
                          "example": false
                        },
                        "needs_coverage": {
                          "type": "boolean",
                          "description": "Whether this shift needs coverage",
                          "example": false
                        },
                        "can_list_for_coverage": {
                          "type": "boolean",
                          "description": "Whether the current user can list this shift for coverage in the marketplace",
                          "example": true
                        },
                        "is_already_listed": {
                          "type": "boolean",
                          "description": "Whether this shift is already listed in the marketplace by the current user",
                          "example": false
                        },
                        "required_users": {
                          "type": "integer",
                          "description": "Number of users required for this shift"
                        },
                        "location": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 1
                            },
                            "name": {
                              "type": "string",
                              "example": "Downtown Store"
                            },
                            "address": {
                              "type": "string",
                              "nullable": true
                            },
                            "phone": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "users": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/User"
                          }
                        },
                        "available_actions": {
                          "type": "array",
                          "description": "Actions available to the current user for this shift",
                          "items": {
                            "type": "string",
                            "enum": [
                              "view_details",
                              "request_coverage",
                              "report_lateness",
                              "view_teammates",
                              "claim"
                            ]
                          },
                          "example": [
                            "view_details",
                            "request_coverage",
                            "view_teammates"
                          ]
                        },
                        "teammates": {
                          "type": "array",
                          "description": "Other users assigned to this shift (when include=teammates)",
                          "nullable": true,
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "description": "User ID"
                              },
                              "name": {
                                "type": "string",
                                "description": "User full name"
                              },
                              "job_title": {
                                "type": "string",
                                "nullable": true,
                                "description": "User's job title/role"
                              },
                              "profile_photo_url": {
                                "type": "string",
                                "nullable": true,
                                "description": "Full-size profile photo URL (200x200) or avatar placeholder"
                              },
                              "profile_photo_thumbnail_url": {
                                "type": "string",
                                "nullable": true,
                                "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                              }
                            }
                          }
                        },
                        "listed_by": {
                          "type": "object",
                          "nullable": true,
                          "description": "User who listed this shift in the marketplace (teammate format, present for marketplace shifts)",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "description": "User ID"
                            },
                            "name": {
                              "type": "string",
                              "description": "User full name"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true,
                              "description": "User's job title/role"
                            },
                            "profile_photo_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200) or avatar placeholder"
                            },
                            "profile_photo_thumbnail_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                            }
                          }
                        },
                        "picked_by": {
                          "type": "object",
                          "nullable": true,
                          "description": "User who claimed/picked up this shift from the marketplace (teammate format, present when shift was claimed)",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "description": "User ID"
                            },
                            "name": {
                              "type": "string",
                              "description": "User full name"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true,
                              "description": "User's job title/role"
                            },
                            "profile_photo_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200) or avatar placeholder"
                            },
                            "profile_photo_thumbnail_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                            }
                          }
                        },
                        "marketplace_info": {
                          "type": "object",
                          "nullable": true,
                          "description": "Marketplace listing information (present when shift has marketplace listing)",
                          "properties": {
                            "has_listing": {
                              "type": "boolean",
                              "description": "Whether this shift has an active marketplace listing",
                              "example": true
                            },
                            "listing_id": {
                              "type": "integer",
                              "description": "ID of the marketplace listing",
                              "example": 880
                            },
                            "listing_status": {
                              "type": "string",
                              "enum": [
                                "open",
                                "filled",
                                "closed",
                                "cancelled"
                              ],
                              "description": "Current status of the marketplace listing",
                              "example": "open"
                            },
                            "listing_type": {
                              "type": "string",
                              "enum": [
                                "pickup",
                                "trade_only",
                                "both"
                              ],
                              "description": "Type of marketplace listing - pickup (direct claim), trade_only (requires application/trade), or both (accepts either)",
                              "example": "pickup"
                            },
                            "price": {
                              "type": "number",
                              "format": "float",
                              "description": "Price offered for the shift",
                              "example": 0.01
                            },
                            "currency": {
                              "type": "string",
                              "description": "Currency code (e.g., USD)",
                              "example": "USD"
                            },
                            "listed_by": {
                              "type": "integer",
                              "description": "User ID of the person who listed the shift (legacy field, use listed_by object instead)",
                              "example": 1487
                            },
                            "listed_at": {
                              "type": "string",
                              "format": "date-time",
                              "description": "When the shift was listed in the marketplace",
                              "example": "2025-12-01T10:05:00Z"
                            }
                          }
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true,
                          "description": "Shift notes (when include=notes)"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "assignment": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "status": {
                          "type": "string",
                          "example": "assigned"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Shift not available for claiming",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "409": {
            "description": "Already assigned to this shift",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/shifts/{id}/actions": {
      "post": {
        "tags": [
          "Shifts"
        ],
        "summary": "Perform shift-related actions",
        "description": "Perform various employee actions on shifts:\n- request_coverage: Request someone to cover this shift\n- report_lateness: Report that you'll be late\n- report_absence: Report absence from shift\n- cancel_request: Cancel a previous request\n\n**Note:** Use `action_type` parameter (preferred) or `action` parameter for the action. \nUsing `action_type` is recommended as `action` is a reserved Rails parameter.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "action_type"
                ],
                "properties": {
                  "action_type": {
                    "type": "string",
                    "enum": [
                      "request_coverage",
                      "report_lateness",
                      "report_absence",
                      "cancel_request"
                    ],
                    "description": "Type of action to perform (preferred parameter)"
                  },
                  "action": {
                    "type": "string",
                    "enum": [
                      "request_coverage",
                      "report_lateness",
                      "report_absence",
                      "cancel_request"
                    ],
                    "description": "Type of action to perform (legacy, use action_type instead)",
                    "deprecated": true
                  },
                  "reason": {
                    "type": "string",
                    "description": "Reason for the action"
                  },
                  "minutes": {
                    "type": "integer",
                    "description": "Minutes late (for report_lateness)"
                  }
                },
                "examples": {
                  "report_lateness": {
                    "summary": "Report lateness",
                    "value": {
                      "action_type": "report_lateness",
                      "minutes": 15,
                      "reason": "Traffic delay"
                    }
                  },
                  "report_absence": {
                    "summary": "Report absence",
                    "value": {
                      "action_type": "report_absence",
                      "reason": "Personal appointment"
                    }
                  },
                  "request_coverage": {
                    "summary": "Request coverage",
                    "value": {
                      "action_type": "request_coverage",
                      "reason": "Family emergency"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Action completed successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "reported_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid action or parameters",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/shift_feedbacks": {
      "get": {
        "tags": [
          "Shifts",
          "Feedback"
        ],
        "summary": "List user's shift feedbacks",
        "description": "Get all shift feedbacks submitted by the authenticated user.\nIncludes feedback details, shift information, and timestamps.\n\nWITHDRAWN submissions (see `DELETE /shift_feedbacks/{id}`) are excluded\nfrom both the list and `meta.stats`.\n",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "description": "Number of items per page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "shift_id",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Filter by specific shift ID"
          },
          {
            "name": "include_stats",
            "in": "query",
            "schema": {
              "type": "boolean"
            },
            "description": "When true, the response `meta` carries a `stats` object for the caller's own feedback in this business (total, average rating, and the positive / neutral / negative / unrated counts, which sum to the total). These figures are always business-wide and are NOT narrowed by `shift_id` — combining the two returns one shift's `items` beside the caller's whole history in `meta.stats`."
          }
        ],
        "responses": {
          "200": {
            "description": "Shift feedbacks retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 789
                          },
                          "attendance_record_id": {
                            "type": "integer",
                            "example": 123,
                            "description": "ID of the associated attendance record"
                          },
                          "user_id": {
                            "type": "integer",
                            "example": 456,
                            "description": "ID of the user who submitted feedback"
                          },
                          "rating": {
                            "type": "integer",
                            "minimum": 1,
                            "maximum": 5,
                            "example": 4,
                            "description": "Overall shift rating (1-5 stars)"
                          },
                          "feedback_text": {
                            "type": "string",
                            "nullable": true,
                            "example": "Great shift, but the workspace was a bit noisy.",
                            "description": "Detailed feedback text (optional)"
                          },
                          "shift_difficulty": {
                            "type": "integer",
                            "minimum": 1,
                            "maximum": 5,
                            "nullable": true,
                            "example": 3,
                            "description": "How difficult was the shift (1=Easy, 5=Very Hard)"
                          },
                          "would_work_again": {
                            "type": "boolean",
                            "nullable": true,
                            "example": true,
                            "description": "Would the user work this shift again"
                          },
                          "can_edit": {
                            "type": "boolean",
                            "example": true,
                            "description": "Whether the feedback can still be edited (within 24 hours)"
                          },
                          "edit_window_expires_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2025-10-11T16:00:00Z",
                            "description": "When the edit window expires (24 hours from submission)"
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2025-10-10T16:00:00Z"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2025-10-10T16:00:00Z"
                          },
                          "attendance_record": {
                            "$ref": "#/components/schemas/AttendanceRecord"
                          },
                          "shift": {
                            "$ref": "#/components/schemas/Shift"
                          },
                          "user": {
                            "$ref": "#/components/schemas/User"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 6
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Items per page",
                          "example": 25
                        }
                      },
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Shifts",
          "Feedback"
        ],
        "summary": "Submit shift feedback",
        "description": "Submit feedback for a completed shift. Can only submit feedback for shifts that:\n- Have been completed (attendance record exists)\n- User was checked in and out\n- Don't already have feedback submitted\n\nRE-SUBMITTING AFTER A WITHDRAWAL restores the withdrawn submission in\nplace: the response is the usual 201, carrying the SAME `id` as the\nwithdrawn one, the newly posted content, and the ORIGINAL\n`submitted_at`. The 24-hour edit/withdraw window therefore runs from the\nFIRST submission and does not restart, and managers are not re-notified.\nAny field you do not send is reset to its default rather than inherited\nfrom the withdrawn submission. This is the ONLY way back in after a\nwithdrawal: `eligible_shifts` and the in-app prompts do not re-offer the\nshift, so a client that withdrew must POST for it explicitly.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "attendance_record_id",
                  "rating"
                ],
                "properties": {
                  "attendance_record_id": {
                    "type": "integer",
                    "description": "ID of the attendance record for the completed shift",
                    "example": 123
                  },
                  "rating": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 5,
                    "description": "Overall shift rating (1-5 stars)",
                    "example": 4
                  },
                  "feedback_text": {
                    "type": "string",
                    "description": "Optional detailed feedback text",
                    "example": "Great shift, but the workspace was a bit noisy.",
                    "maxLength": 2000
                  },
                  "shift_difficulty": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 5,
                    "description": "How difficult was the shift (1=Easy, 5=Very Hard)",
                    "example": 3
                  },
                  "would_work_again": {
                    "type": "boolean",
                    "description": "Would you work this shift again?",
                    "example": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Shift feedback created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Shift feedback submitted successfully"
                    },
                    "shift_feedback": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 789
                        },
                        "attendance_record_id": {
                          "type": "integer",
                          "example": 123,
                          "description": "ID of the associated attendance record"
                        },
                        "user_id": {
                          "type": "integer",
                          "example": 456,
                          "description": "ID of the user who submitted feedback"
                        },
                        "rating": {
                          "type": "integer",
                          "minimum": 1,
                          "maximum": 5,
                          "example": 4,
                          "description": "Overall shift rating (1-5 stars)"
                        },
                        "feedback_text": {
                          "type": "string",
                          "nullable": true,
                          "example": "Great shift, but the workspace was a bit noisy.",
                          "description": "Detailed feedback text (optional)"
                        },
                        "shift_difficulty": {
                          "type": "integer",
                          "minimum": 1,
                          "maximum": 5,
                          "nullable": true,
                          "example": 3,
                          "description": "How difficult was the shift (1=Easy, 5=Very Hard)"
                        },
                        "would_work_again": {
                          "type": "boolean",
                          "nullable": true,
                          "example": true,
                          "description": "Would the user work this shift again"
                        },
                        "can_edit": {
                          "type": "boolean",
                          "example": true,
                          "description": "Whether the feedback can still be edited (within 24 hours)"
                        },
                        "edit_window_expires_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2025-10-11T16:00:00Z",
                          "description": "When the edit window expires (24 hours from submission)"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2025-10-10T16:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2025-10-10T16:00:00Z"
                        },
                        "attendance_record": {
                          "$ref": "#/components/schemas/AttendanceRecord"
                        },
                        "shift": {
                          "$ref": "#/components/schemas/Shift"
                        },
                        "user": {
                          "$ref": "#/components/schemas/User"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request (missing params, invalid rating, etc.)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Attendance record not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          }
        }
      }
    },
    "/shift_feedbacks/{id}": {
      "get": {
        "tags": [
          "Shifts",
          "Feedback"
        ],
        "summary": "Get specific shift feedback",
        "description": "Retrieve details of a specific shift feedback",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "Shift feedback ID"
          }
        ],
        "responses": {
          "200": {
            "description": "Shift feedback retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "shift_feedback": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 789
                        },
                        "attendance_record_id": {
                          "type": "integer",
                          "example": 123,
                          "description": "ID of the associated attendance record"
                        },
                        "user_id": {
                          "type": "integer",
                          "example": 456,
                          "description": "ID of the user who submitted feedback"
                        },
                        "rating": {
                          "type": "integer",
                          "minimum": 1,
                          "maximum": 5,
                          "example": 4,
                          "description": "Overall shift rating (1-5 stars)"
                        },
                        "feedback_text": {
                          "type": "string",
                          "nullable": true,
                          "example": "Great shift, but the workspace was a bit noisy.",
                          "description": "Detailed feedback text (optional)"
                        },
                        "shift_difficulty": {
                          "type": "integer",
                          "minimum": 1,
                          "maximum": 5,
                          "nullable": true,
                          "example": 3,
                          "description": "How difficult was the shift (1=Easy, 5=Very Hard)"
                        },
                        "would_work_again": {
                          "type": "boolean",
                          "nullable": true,
                          "example": true,
                          "description": "Would the user work this shift again"
                        },
                        "can_edit": {
                          "type": "boolean",
                          "example": true,
                          "description": "Whether the feedback can still be edited (within 24 hours)"
                        },
                        "edit_window_expires_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2025-10-11T16:00:00Z",
                          "description": "When the edit window expires (24 hours from submission)"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2025-10-10T16:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2025-10-10T16:00:00Z"
                        },
                        "attendance_record": {
                          "$ref": "#/components/schemas/AttendanceRecord"
                        },
                        "shift": {
                          "$ref": "#/components/schemas/Shift"
                        },
                        "user": {
                          "$ref": "#/components/schemas/User"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Refused. `app_not_enabled` / `app_access_denied` (the tenant has not\nenabled the app, or this user is outside its audience), or\n`insufficient_permissions` (the token lacks `read:shift_feedback`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "`feedback_not_found` — no such feedback for the authenticated user\nin this business. A feedback belonging to ANOTHER user answers 404,\nnot 403. A WITHDRAWN submission also answers 404.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Shifts",
          "Feedback"
        ],
        "summary": "Update shift feedback",
        "description": "Update a previously submitted shift feedback.\nCan only update within 24 hours of submission.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "Shift feedback ID"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "rating": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 5,
                    "description": "Overall shift rating (1-5 stars)"
                  },
                  "feedback_text": {
                    "type": "string",
                    "description": "Detailed feedback text",
                    "maxLength": 2000
                  },
                  "shift_difficulty": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 5,
                    "description": "How difficult was the shift"
                  },
                  "would_work_again": {
                    "type": "boolean",
                    "description": "Would you work this shift again?"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Shift feedback updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Shift feedback updated successfully"
                    },
                    "shift_feedback": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 789
                        },
                        "attendance_record_id": {
                          "type": "integer",
                          "example": 123,
                          "description": "ID of the associated attendance record"
                        },
                        "user_id": {
                          "type": "integer",
                          "example": 456,
                          "description": "ID of the user who submitted feedback"
                        },
                        "rating": {
                          "type": "integer",
                          "minimum": 1,
                          "maximum": 5,
                          "example": 4,
                          "description": "Overall shift rating (1-5 stars)"
                        },
                        "feedback_text": {
                          "type": "string",
                          "nullable": true,
                          "example": "Great shift, but the workspace was a bit noisy.",
                          "description": "Detailed feedback text (optional)"
                        },
                        "shift_difficulty": {
                          "type": "integer",
                          "minimum": 1,
                          "maximum": 5,
                          "nullable": true,
                          "example": 3,
                          "description": "How difficult was the shift (1=Easy, 5=Very Hard)"
                        },
                        "would_work_again": {
                          "type": "boolean",
                          "nullable": true,
                          "example": true,
                          "description": "Would the user work this shift again"
                        },
                        "can_edit": {
                          "type": "boolean",
                          "example": true,
                          "description": "Whether the feedback can still be edited (within 24 hours)"
                        },
                        "edit_window_expires_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2025-10-11T16:00:00Z",
                          "description": "When the edit window expires (24 hours from submission)"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2025-10-10T16:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2025-10-10T16:00:00Z"
                        },
                        "attendance_record": {
                          "$ref": "#/components/schemas/AttendanceRecord"
                        },
                        "shift": {
                          "$ref": "#/components/schemas/Shift"
                        },
                        "user": {
                          "$ref": "#/components/schemas/User"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`parameter_missing` — the request body is missing the\n`shift_feedback` object, or sends a non-scalar value for a scalar\nfield. The expired edit window is a 403, not a 400.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Refused. `update_not_allowed` (more than 24 hours since\nsubmission), `feedback_collection_disabled`, `app_not_enabled` /\n`app_access_denied`, or `insufficient_permissions` (the token lacks\n`write:shift_feedback`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "`feedback_not_found` — no such feedback for the authenticated user\nin this business. A feedback belonging to ANOTHER user answers 404,\nnot 403.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Shifts",
          "Feedback"
        ],
        "summary": "Withdraw shift feedback",
        "description": "Withdraw a previously submitted shift feedback.\nCan only withdraw within 24 hours of submission.\n\nThe submission is RETAINED, not erased: it is marked withdrawn and\ndisappears from every read surface (this API's list and detail\nendpoints, the in-app feedback lists and CSV exports, search, reporting\nand the engagement signal it fed), and it stops counting toward the\ntenant's response rate. Nothing about the request or the response\nchanges — this note exists so integrators do not treat a 200 here as\nproof the data is gone.\n\nRe-submitting feedback for the same attendance record afterwards\nRESTORES the withdrawn submission in place: `POST /shift_feedbacks`\nreturns 201 with the SAME `id`, carrying the newly posted content and\nthe ORIGINAL `submitted_at` — so the 24-hour edit/withdraw window runs\nfrom the first submission and does not restart.\n\nThe shift is NOT put back on `GET /shift_feedbacks/eligible_shifts`, and\nit is not re-offered on any in-app \"rate your shift\" prompt. A withdrawal\nis a decision the app records, so re-submitting is a deliberate POST by\nthe client that withdrew — not something the worker is prompted for\nagain.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "Shift feedback ID"
          }
        ],
        "responses": {
          "200": {
            "description": "Shift feedback withdrawn successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Feedback deleted successfully"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Refused. `deletion_not_allowed` (more than 24 hours since\nsubmission), `feedback_collection_disabled` (the tenant has turned\nShift Feedback collection off), `app_not_enabled` /\n`app_access_denied` (the tenant has not enabled the app, or this\nuser is outside its audience), or `insufficient_permissions`\n(the token lacks `write:shift_feedback`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "`feedback_not_found` — no such feedback for the authenticated user\nin this business. A feedback belonging to ANOTHER user answers 404,\nnot 403: the lookup is scoped to the caller's own submissions and\nnever discloses that the id exists.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/shift_feedbacks/eligible_shifts": {
      "get": {
        "tags": [
          "Shifts",
          "Feedback"
        ],
        "summary": "Get shifts eligible for feedback",
        "description": "Get a list of completed shifts that are eligible for feedback submission.\nOnly returns shifts that:\n- Have been completed (user was checked in and out)\n- Don't already have feedback submitted — a WITHDRAWN submission still\n  counts as submitted here, so a shift whose feedback was withdrawn is\n  not re-offered\n- Belong to the authenticated user\n\nReturns the tenant's configured number of most recent eligible shifts\n(the `max_feedback_shifts` setting: 3 by default, 10 at most).\n",
        "responses": {
          "200": {
            "description": "Eligible shifts retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "eligible_shifts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "attendance_record_id": {
                            "type": "integer",
                            "example": 123
                          },
                          "shift_id": {
                            "type": "integer",
                            "example": 456
                          },
                          "shift_name": {
                            "type": "string",
                            "example": "Morning Shift - Location A"
                          },
                          "shift_date": {
                            "type": "string",
                            "format": "date",
                            "example": "2025-10-09"
                          },
                          "shift_start_time": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2025-10-09T08:00:00Z"
                          },
                          "check_in_time": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2025-10-09T08:05:00Z"
                          },
                          "check_out_time": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2025-10-09T16:00:00Z"
                          },
                          "hours_worked": {
                            "type": "number",
                            "format": "float",
                            "example": 7.92
                          }
                        }
                      }
                    },
                    "count": {
                      "type": "integer",
                      "description": "Number of eligible shifts",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/missing_punch_requests": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "List the caller's missing-punch requests",
        "description": "The authenticated employee's own punch-correction requests, newest first.\n\nDeliberately self-scoped — this is the employee surface. The manager\nreview queue is `GET /api/v1/attendance_records/requires_review`, which\ncarries its own role gate.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "description": "Number of items per page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "pending"
              ]
            },
            "description": "`pending` returns only requests still awaiting review — the set the caller can still cancel. Any other value is ignored rather than rejected, so new values can be added without breaking old clients.\n"
          }
        ],
        "responses": {
          "200": {
            "description": "Requests retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "An employee-submitted punch correction. Stored as an AttendanceRecord with punch_source='missing_punch_request' and flagged for manager review — it is a REQUEST, not an approved punch, until a manager acts on it.\n",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "status": {
                            "type": "string",
                            "description": "`pending_review` until a manager acts; afterwards the underlying attendance status.\n"
                          },
                          "pending_review": {
                            "type": "boolean"
                          },
                          "check_in_time": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "check_out_time": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "reason": {
                            "type": "string",
                            "description": "The employee's stated reason for the correction."
                          },
                          "reviewed_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "can_cancel": {
                            "type": "boolean",
                            "description": "Whether DELETE would succeed — true only while no manager has acted. Use it to decide whether to show a cancel control instead of discovering the rule from a 422.\n"
                          },
                          "shift_id": {
                            "type": "integer",
                            "description": "Detailed responses only (create). The supporting ad-hoc shift."
                          },
                          "location_name": {
                            "type": "string",
                            "nullable": true,
                            "description": "Detailed responses only."
                          },
                          "breaks": {
                            "type": "array",
                            "description": "Detailed responses only. Required break types the employee did not submit appear with status `skipped`.\n",
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "type": "integer"
                                },
                                "break_type_id": {
                                  "type": "integer"
                                },
                                "break_type_name": {
                                  "type": "string"
                                },
                                "start_time": {
                                  "type": "string",
                                  "format": "date-time",
                                  "nullable": true
                                },
                                "end_time": {
                                  "type": "string",
                                  "format": "date-time",
                                  "nullable": true
                                },
                                "duration_minutes": {
                                  "type": "integer",
                                  "nullable": true
                                },
                                "status": {
                                  "type": "string"
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 6
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Items per page",
                          "example": 25
                        }
                      },
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Time & Attendance is not enabled or not accessible to this user"
          }
        }
      },
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "Submit a missing-punch request",
        "description": "File a punch correction for a day the clock missed. Creates an\nAttendanceRecord flagged for manager review (it is NOT an approved\npunch), plus a supporting ad-hoc shift, and notifies the managers who\nhave to review it.\n\nAll rules are shared with the web form — reason bounds, maximum span,\nthe retroactive window, and break-row validation. Call\n`GET /missing_punch_requests/window` first and build the picker from\nthose bounds so the client cannot offer a date this endpoint refuses.\n\nTimes are parsed server-side: an unparseable value is refused, never\ncast to null.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "check_in_time",
                  "check_out_time",
                  "reason"
                ],
                "properties": {
                  "check_in_time": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Must be in the past and inside the allowed window."
                  },
                  "check_out_time": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Must be after check_in_time. May fall on the following day — overnights are allowed — but the total span is capped (see max_span_hours on the window endpoint).\n"
                  },
                  "reason": {
                    "type": "string",
                    "description": "Why the punch is missing. Length bounds come from the window endpoint."
                  },
                  "timesheet_id": {
                    "type": "integer",
                    "description": "Optional. Narrows the allowed window to that timesheet's pay period. An id that is not the caller's own is ignored rather than borrowing another employee's period.\n"
                  },
                  "breaks": {
                    "type": "array",
                    "description": "Optional break rows. Each must sit inside the punch window and carry both times or neither. Required break types the caller omits are recorded as skipped.\n",
                    "items": {
                      "type": "object",
                      "properties": {
                        "break_type_id": {
                          "type": "integer"
                        },
                        "start_time": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "end_time": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Request submitted and pending manager review",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "missing_punch_request": {
                      "type": "object",
                      "description": "An employee-submitted punch correction. Stored as an AttendanceRecord with punch_source='missing_punch_request' and flagged for manager review — it is a REQUEST, not an approved punch, until a manager acts on it.\n",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "status": {
                          "type": "string",
                          "description": "`pending_review` until a manager acts; afterwards the underlying attendance status.\n"
                        },
                        "pending_review": {
                          "type": "boolean"
                        },
                        "check_in_time": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "check_out_time": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "reason": {
                          "type": "string",
                          "description": "The employee's stated reason for the correction."
                        },
                        "reviewed_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "can_cancel": {
                          "type": "boolean",
                          "description": "Whether DELETE would succeed — true only while no manager has acted. Use it to decide whether to show a cancel control instead of discovering the rule from a 422.\n"
                        },
                        "shift_id": {
                          "type": "integer",
                          "description": "Detailed responses only (create). The supporting ad-hoc shift."
                        },
                        "location_name": {
                          "type": "string",
                          "nullable": true,
                          "description": "Detailed responses only."
                        },
                        "breaks": {
                          "type": "array",
                          "description": "Detailed responses only. Required break types the employee did not submit appear with status `skipped`.\n",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "break_type_id": {
                                "type": "integer"
                              },
                              "break_type_name": {
                                "type": "string"
                              },
                              "start_time": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true
                              },
                              "end_time": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true
                              },
                              "duration_minutes": {
                                "type": "integer",
                                "nullable": true
                              },
                              "status": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      }
                    },
                    "managers_notified": {
                      "type": "boolean",
                      "description": "Whether any manager was actually reached. FALSE is a real outcome — a location supervised only by an admin can resolve no manager recipient — so do not tell the employee their request was sent to a manager unless this is true.\n"
                    },
                    "notification_status": {
                      "type": "string",
                      "enum": [
                        "notified",
                        "no_recipients",
                        "delivery_failed"
                      ],
                      "description": "Why `managers_notified` is what it is. Prefer this over the boolean when wording the confirmation, because `no_recipients` and `delivery_failed` need DIFFERENT copy: `no_recipients` means no manager is assigned to the location (tell them to contact HR), while `delivery_failed` means the request IS in their manager's review queue and only the alert was lost (do not send them to HR). The request itself is created and pending review in all three cases.\n"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Time & Attendance is not enabled or not accessible to this user"
          },
          "422": {
            "description": "Validation failure. `error.code` is `validation_failed` for input the employee can fix (reason length, span, window, break rows) or `record_invalid` when the record itself was rejected. `error.message` is the sentence to show them.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/missing_punch_requests/window": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "Bounds and limits for building the missing-punch form",
        "description": "Everything a client needs to render the form so it cannot offer a value\nthe submit endpoint refuses: the allowed datetime range, whether the\nwindow is open at all, the reason/span limits, and the required break\nrows. Read off the same object that validates the submit.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "timesheet_id",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Narrow the window to that timesheet's pay period."
          }
        ],
        "responses": {
          "200": {
            "description": "Window resolved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "window": {
                      "type": "object",
                      "properties": {
                        "open": {
                          "type": "boolean",
                          "description": "FALSE when the pay period and the retroactive window do not overlap — there is no date the submit endpoint would accept. Withhold the form and show closed_reason.\n"
                        },
                        "min_time": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Earliest allowed clock-in. Null means no lower bound."
                        },
                        "max_time": {
                          "type": "string",
                          "format": "date-time",
                          "description": "Latest allowed clock-in (never in the future)."
                        },
                        "check_out_max_time": {
                          "type": "string",
                          "format": "date-time",
                          "description": "Ceiling for clock-OUT, deliberately later than max_time so a shift starting on the period's last day can end the next morning.\n"
                        },
                        "scoped_to_timesheet": {
                          "type": "boolean"
                        },
                        "timesheet_period": {
                          "type": "string",
                          "nullable": true
                        },
                        "lockout_days": {
                          "type": "integer",
                          "description": "The tenant's retroactive correction window, in days."
                        },
                        "range_sentence": {
                          "type": "string",
                          "nullable": true,
                          "description": "The allowed range in one sentence, shared with the web form's helper text and the server's rejection so no surface states a different rule. NULL when open is false — a closed window's minimum is later than its maximum, so there is no coherent range to state; use closed_reason instead.\n"
                        },
                        "closed_reason": {
                          "type": "string",
                          "nullable": true,
                          "description": "Why the window is closed. Null when open."
                        },
                        "reason_min_length": {
                          "type": "integer"
                        },
                        "reason_max_length": {
                          "type": "integer"
                        },
                        "max_span_hours": {
                          "type": "integer"
                        },
                        "required_break_types": {
                          "type": "array",
                          "description": "Required break types only — an optional break the employee did not take needs no row.\n",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string"
                              },
                              "duration_minutes": {
                                "type": "integer"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Time & Attendance is not enabled or not accessible to this user"
          }
        }
      }
    },
    "/missing_punch_requests/{id}": {
      "delete": {
        "tags": [
          "Attendance"
        ],
        "summary": "Withdraw a pending missing-punch request",
        "description": "Cancel a request no manager has acted on yet, removing the supporting\nad-hoc shift with it. A request belonging to another employee, or one\nalready reviewed, is refused — use `can_cancel` on the request to decide\nwhether to show the control.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Request cancelled",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Time & Attendance is not enabled or not accessible to this user"
          },
          "409": {
            "description": "`cancel_blocked` — other records (lone-worker check-ins or alerts, attendance notifications) still reference this request, so it can never be cancelled by retrying. Ask a manager to reject it instead.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "422": {
            "description": "`not_cancellable` — already reviewed, not a missing-punch request, or not the caller's own.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/break_types": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "List break types",
        "description": "The tenant's break types, for a break picker. Without this, a client can\nonly see breaks already planned on the current shift (the\n`break_records`/`break_type` embeds on the attendance record), so a\ntenant whose optional break types are not attached to shifts shows the\nemployee an empty break list.\n\nThe field set matches the `break_type` embed on the attendance record\nexactly, so one client-side model decodes both. Required types are\nincluded rather than filtered out — `is_required` is present precisely\nso the client can split required-and-auto-attached from\noptional-and-startable itself.\n\nNot paginated: a tenant's break-type count is naturally small and a\npicker needs the full set.\n",
        "responses": {
          "200": {
            "description": "The tenant's break types, ordered by name",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "break_types": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 5
                          },
                          "name": {
                            "type": "string",
                            "example": "Meal Break"
                          },
                          "duration_minutes": {
                            "type": "integer",
                            "example": 30
                          },
                          "is_required": {
                            "type": "boolean",
                            "example": true
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer",
                          "example": 4
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/attendance_records": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "List attendance records",
        "description": "Returns a paginated list of attendance records with optional filters for user, shift, and status.\nUse this to retrieve historical attendance activity for reporting and audits.\n",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "description": "Number of items per page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "user_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "shift_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by attendance status. Comma-separated values are accepted. These are the real AttendanceRecord statuses; `approved`/`rejected` were previously documented here but are review outcomes on a different column, so requesting them returned an empty list forever.",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "on_time",
                "late",
                "missed",
                "completed",
                "absence_reported"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of attendance records",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "user_id": {
                            "type": "integer"
                          },
                          "shift_id": {
                            "type": "integer"
                          },
                          "business_id": {
                            "type": "integer"
                          },
                          "check_in_time": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "check_out_time": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "pending",
                              "on_time",
                              "late",
                              "missed",
                              "completed",
                              "absence_reported",
                              "excused",
                              "no_show"
                            ]
                          },
                          "location_verified": {
                            "type": "boolean",
                            "default": false
                          },
                          "photo_verification_required": {
                            "type": "boolean",
                            "default": false
                          },
                          "photo_verification_failed_reason": {
                            "type": "string",
                            "nullable": true
                          },
                          "is_unknown_device": {
                            "type": "boolean",
                            "default": false
                          },
                          "requires_review": {
                            "type": "boolean",
                            "default": false
                          },
                          "review_reason": {
                            "type": "string",
                            "nullable": true
                          },
                          "reviewed_by_user_id": {
                            "type": "integer",
                            "nullable": true
                          },
                          "reviewed_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "excused_by_user_id": {
                            "type": "integer",
                            "nullable": true
                          },
                          "excused_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "absence_report_id": {
                            "type": "integer",
                            "nullable": true
                          },
                          "notes": {
                            "type": "string",
                            "nullable": true
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "user": {
                            "$ref": "#/components/schemas/User"
                          },
                          "shift": {
                            "allOf": [
                              {
                                "$ref": "#/components/schemas/Shift"
                              }
                            ],
                            "nullable": true,
                            "description": "The record's shift. A SUBSET of the Shift schema: id, name, start_time, end_time, formatted_date, formatted_time — plus status, location and location_department when `include=shift_details` is requested. `formatted_date` (\"Wednesday, Aug 05\") and `formatted_time` (\"01:30 PM - 09:30 PM\") are rendered in the SHIFT's own timezone, are byte-identical to what `GET /shifts` returns for the same shift.id, and do not vary with the caller's profile timezone — clients should prefer them over formatting `start_time` locally, which has no usable zone once an SDK decodes it to a bare instant. `formatted_date` always names the shift's START day, so it is correct for a shift crossing midnight."
                          },
                          "break_records": {
                            "type": "array",
                            "items": {
                              "$ref": "#/components/schemas/BreakRecord"
                            }
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 6
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Items per page",
                          "example": 25
                        }
                      },
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "Create attendance record (check-in)",
        "description": "Creates a new attendance record representing a check-in event. If `shift_id` is omitted or null,\nthe system creates an adhoc shift at the user's primary location and associates the record automatically.\n\n**Adhoc Clock-in**: When shift_id is null, the system will:\n1. Resolve the location — `location_id` when supplied, otherwise the\n   user's primary assigned location, else their assigned active\n   locations by name, else the business's active locations by name\n2. Create an adhoc shift with no predetermined end time\n3. Create a shift assignment for the user\n4. Create the attendance record with the new shift_id\n\nSupply `location_id` to let the employee choose where they are punching\nin; `GET /locations` returns exactly the set that is accepted. A\n`location_id` outside that set is REFUSED with 422 rather than being\nignored — a chooser whose choice is silently discarded would land the\npunch at a site the employee did not pick.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "user_id",
                  "check_in_time"
                ],
                "properties": {
                  "user_id": {
                    "type": "integer",
                    "description": "ID of the user checking in"
                  },
                  "shift_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "ID of the shift to check in for. If null, an adhoc shift will be created automatically."
                  },
                  "check_in_time": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Time of check-in"
                  },
                  "location_id": {
                    "type": "integer",
                    "description": "Adhoc clock-in only (ignored when shift_id is supplied — a scheduled punch takes its shift's location). Must be one of the caller's assigned ACTIVE locations for this business; anything else is refused with 422. Omit to accept the resolved default described above. Feed the picker from `GET /locations`."
                  },
                  "location": {
                    "type": "string",
                    "description": "Location description"
                  },
                  "device_info": {
                    "type": "string",
                    "description": "Device information"
                  },
                  "notes": {
                    "type": "string",
                    "description": "Additional notes"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Attendance record created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "user_id": {
                      "type": "integer"
                    },
                    "shift_id": {
                      "type": "integer"
                    },
                    "business_id": {
                      "type": "integer"
                    },
                    "check_in_time": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "check_out_time": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "on_time",
                        "late",
                        "missed",
                        "completed",
                        "absence_reported",
                        "excused",
                        "no_show"
                      ]
                    },
                    "location_verified": {
                      "type": "boolean",
                      "default": false
                    },
                    "photo_verification_required": {
                      "type": "boolean",
                      "default": false
                    },
                    "photo_verification_failed_reason": {
                      "type": "string",
                      "nullable": true
                    },
                    "is_unknown_device": {
                      "type": "boolean",
                      "default": false
                    },
                    "requires_review": {
                      "type": "boolean",
                      "default": false
                    },
                    "review_reason": {
                      "type": "string",
                      "nullable": true
                    },
                    "reviewed_by_user_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "reviewed_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "excused_by_user_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "excused_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "absence_report_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "notes": {
                      "type": "string",
                      "nullable": true
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "updated_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "user": {
                      "$ref": "#/components/schemas/User"
                    },
                    "shift": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Shift"
                        }
                      ],
                      "nullable": true,
                      "description": "The record's shift. A SUBSET of the Shift schema: id, name, start_time, end_time, formatted_date, formatted_time — plus status, location and location_department when `include=shift_details` is requested. `formatted_date` (\"Wednesday, Aug 05\") and `formatted_time` (\"01:30 PM - 09:30 PM\") are rendered in the SHIFT's own timezone, are byte-identical to what `GET /shifts` returns for the same shift.id, and do not vary with the caller's profile timezone — clients should prefer them over formatting `start_time` locally, which has no usable zone once an SDK decodes it to a bare instant. `formatted_date` always names the shift's START day, so it is correct for a shift crossing midnight."
                    },
                    "break_records": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BreakRecord"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation failed. For adhoc clock-in, `errors[].field` =\n`location_id` means either the supplied location is not one of the\ncaller's assigned active locations, or none was supplied and the\nuser has no assignable location to fall back to.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "Refused. Either the caller may not create a record for the named\nemployee, or — `error.code` = `kiosk_mode_enabled` — the business\nruns a shared Time Clock Kiosk and self-service punching from a\npersonal device is turned off. The kiosk refusal carries\n`error.details.time_clock_kiosk_enabled: true` and applies only when\nthe record is for the caller themselves; a manager recording a punch\nfor a subordinate is unaffected. Not retryable.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/attendance_records/{id}/check_out": {
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "Check out from shift",
        "description": "Completes an attendance record by recording a check-out time and optional metadata such as notes\nand location. Returns the updated attendance record.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "check_out_time": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "location": {
                    "type": "string"
                  },
                  "notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successfully checked out. Alongside `attendance_record`, the body\ncarries an `undo` object describing the Undo Clock-Out window that\njust opened — render the countdown from `undo_seconds_remaining`\n(measured against the server clock, so a device with clock drift\nstill counts down correctly) and POST\n`/attendance_records/{id}/undo_clock_out` while it is above zero.\n`GET /attendance_records/status` carries the same `undo` object, so\na client that relaunches inside the window can rebuild the card.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "attendance_record": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "user_id": {
                          "type": "integer"
                        },
                        "shift_id": {
                          "type": "integer"
                        },
                        "business_id": {
                          "type": "integer"
                        },
                        "check_in_time": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "check_out_time": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "on_time",
                            "late",
                            "missed",
                            "completed",
                            "absence_reported",
                            "excused",
                            "no_show"
                          ]
                        },
                        "location_verified": {
                          "type": "boolean",
                          "default": false
                        },
                        "photo_verification_required": {
                          "type": "boolean",
                          "default": false
                        },
                        "photo_verification_failed_reason": {
                          "type": "string",
                          "nullable": true
                        },
                        "is_unknown_device": {
                          "type": "boolean",
                          "default": false
                        },
                        "requires_review": {
                          "type": "boolean",
                          "default": false
                        },
                        "review_reason": {
                          "type": "string",
                          "nullable": true
                        },
                        "reviewed_by_user_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "reviewed_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "excused_by_user_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "excused_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "absence_report_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "user": {
                          "$ref": "#/components/schemas/User"
                        },
                        "shift": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/Shift"
                            }
                          ],
                          "nullable": true,
                          "description": "The record's shift. A SUBSET of the Shift schema: id, name, start_time, end_time, formatted_date, formatted_time — plus status, location and location_department when `include=shift_details` is requested. `formatted_date` (\"Wednesday, Aug 05\") and `formatted_time` (\"01:30 PM - 09:30 PM\") are rendered in the SHIFT's own timezone, are byte-identical to what `GET /shifts` returns for the same shift.id, and do not vary with the caller's profile timezone — clients should prefer them over formatting `start_time` locally, which has no usable zone once an SDK decodes it to a bare instant. `formatted_date` always names the shift's START day, so it is correct for a shift crossing midnight."
                        },
                        "break_records": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/BreakRecord"
                          }
                        }
                      }
                    },
                    "undo": {
                      "$ref": "#/components/schemas/UndoClockOutWindow"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`error.code` = `kiosk_mode_enabled` — the business runs a shared Time\nClock Kiosk, so clocking out from a personal device is turned off.\nApplies to the caller's own record only; carries\n`error.details.time_clock_kiosk_enabled: true`. Not retryable — the\nemployee must clock out at the kiosk.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/attendance_records/{id}/undo_clock_out": {
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "Undo a clock-out",
        "description": "Reverses the CALLER'S OWN clock-out within the 15-minute undo window,\nreopening the punch so they can keep working. Self-service only: a\nmanager correcting someone else's punch uses\n`PATCH /attendance_records/{id}/adjust`, which is audited as a manager\nedit. Refused once the caller has clocked in again.\n\nSend an `Idempotency-Key` header to make a network retry safe — a replay\nreturns the original response with `X-Idempotency-Cached: true` rather\nthan failing as an expired window.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Clock-out undone; the caller is clocked back in. `undo` comes back\nwith `can_undo_clock_out: false` because the punch is open again.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "attendance_record": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "user_id": {
                          "type": "integer"
                        },
                        "shift_id": {
                          "type": "integer"
                        },
                        "business_id": {
                          "type": "integer"
                        },
                        "check_in_time": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "check_out_time": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "on_time",
                            "late",
                            "missed",
                            "completed",
                            "absence_reported",
                            "excused",
                            "no_show"
                          ]
                        },
                        "location_verified": {
                          "type": "boolean",
                          "default": false
                        },
                        "photo_verification_required": {
                          "type": "boolean",
                          "default": false
                        },
                        "photo_verification_failed_reason": {
                          "type": "string",
                          "nullable": true
                        },
                        "is_unknown_device": {
                          "type": "boolean",
                          "default": false
                        },
                        "requires_review": {
                          "type": "boolean",
                          "default": false
                        },
                        "review_reason": {
                          "type": "string",
                          "nullable": true
                        },
                        "reviewed_by_user_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "reviewed_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "excused_by_user_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "excused_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "absence_report_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "user": {
                          "$ref": "#/components/schemas/User"
                        },
                        "shift": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/Shift"
                            }
                          ],
                          "nullable": true,
                          "description": "The record's shift. A SUBSET of the Shift schema: id, name, start_time, end_time, formatted_date, formatted_time — plus status, location and location_department when `include=shift_details` is requested. `formatted_date` (\"Wednesday, Aug 05\") and `formatted_time` (\"01:30 PM - 09:30 PM\") are rendered in the SHIFT's own timezone, are byte-identical to what `GET /shifts` returns for the same shift.id, and do not vary with the caller's profile timezone — clients should prefer them over formatting `start_time` locally, which has no usable zone once an SDK decodes it to a bare instant. `formatted_date` always names the shift's START day, so it is correct for a shift crossing midnight."
                        },
                        "break_records": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/BreakRecord"
                          }
                        }
                      }
                    },
                    "undo": {
                      "$ref": "#/components/schemas/UndoClockOutWindow"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`error.code` = `not_your_record` — the record belongs to someone\nelse. Or `kiosk_mode_enabled` — the business runs a shared Time Clock\nKiosk, so reversing a punch from a personal device is turned off.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "422": {
            "description": "`error.code` = `undo_window_expired` — the window has passed, the\nrecord is not a completed clock-out, or shift feedback was already\nsubmitted; `error.details` carries the same `undo` fields so the\nclient can re-render the card. Or `already_clocked_in` — the caller\nhas since started another punch (`error.details.open_attendance_record_id`).\nOr `undo_clock_out_failed` — the reversal was refused on save.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/attendance_records/{id}": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "Get attendance record details",
        "description": "Retrieve a single attendance record by ID, including times and status.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Attendance record details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "attendance_record": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "user_id": {
                          "type": "integer"
                        },
                        "shift_id": {
                          "type": "integer"
                        },
                        "business_id": {
                          "type": "integer"
                        },
                        "check_in_time": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "check_out_time": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "on_time",
                            "late",
                            "missed",
                            "completed",
                            "absence_reported",
                            "excused",
                            "no_show"
                          ]
                        },
                        "location_verified": {
                          "type": "boolean",
                          "default": false
                        },
                        "photo_verification_required": {
                          "type": "boolean",
                          "default": false
                        },
                        "photo_verification_failed_reason": {
                          "type": "string",
                          "nullable": true
                        },
                        "is_unknown_device": {
                          "type": "boolean",
                          "default": false
                        },
                        "requires_review": {
                          "type": "boolean",
                          "default": false
                        },
                        "review_reason": {
                          "type": "string",
                          "nullable": true
                        },
                        "reviewed_by_user_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "reviewed_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "excused_by_user_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "excused_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "absence_report_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "user": {
                          "$ref": "#/components/schemas/User"
                        },
                        "shift": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/Shift"
                            }
                          ],
                          "nullable": true,
                          "description": "The record's shift. A SUBSET of the Shift schema: id, name, start_time, end_time, formatted_date, formatted_time — plus status, location and location_department when `include=shift_details` is requested. `formatted_date` (\"Wednesday, Aug 05\") and `formatted_time` (\"01:30 PM - 09:30 PM\") are rendered in the SHIFT's own timezone, are byte-identical to what `GET /shifts` returns for the same shift.id, and do not vary with the caller's profile timezone — clients should prefer them over formatting `start_time` locally, which has no usable zone once an SDK decodes it to a bare instant. `formatted_date` always names the shift's START day, so it is correct for a shift crossing midnight."
                        },
                        "break_records": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/BreakRecord"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Attendance"
        ],
        "summary": "Update attendance record",
        "description": "Update an existing attendance record's times, status, or notes.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "check_in_time": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "check_out_time": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "pending",
                      "on_time",
                      "late",
                      "missed",
                      "completed",
                      "absence_reported",
                      "excused",
                      "no_show"
                    ]
                  },
                  "notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Attendance record updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "attendance_record": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "user_id": {
                          "type": "integer"
                        },
                        "shift_id": {
                          "type": "integer"
                        },
                        "business_id": {
                          "type": "integer"
                        },
                        "check_in_time": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "check_out_time": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "on_time",
                            "late",
                            "missed",
                            "completed",
                            "absence_reported",
                            "excused",
                            "no_show"
                          ]
                        },
                        "location_verified": {
                          "type": "boolean",
                          "default": false
                        },
                        "photo_verification_required": {
                          "type": "boolean",
                          "default": false
                        },
                        "photo_verification_failed_reason": {
                          "type": "string",
                          "nullable": true
                        },
                        "is_unknown_device": {
                          "type": "boolean",
                          "default": false
                        },
                        "requires_review": {
                          "type": "boolean",
                          "default": false
                        },
                        "review_reason": {
                          "type": "string",
                          "nullable": true
                        },
                        "reviewed_by_user_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "reviewed_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "excused_by_user_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "excused_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "absence_report_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "user": {
                          "$ref": "#/components/schemas/User"
                        },
                        "shift": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/Shift"
                            }
                          ],
                          "nullable": true,
                          "description": "The record's shift. A SUBSET of the Shift schema: id, name, start_time, end_time, formatted_date, formatted_time — plus status, location and location_department when `include=shift_details` is requested. `formatted_date` (\"Wednesday, Aug 05\") and `formatted_time` (\"01:30 PM - 09:30 PM\") are rendered in the SHIFT's own timezone, are byte-identical to what `GET /shifts` returns for the same shift.id, and do not vary with the caller's profile timezone — clients should prefer them over formatting `start_time` locally, which has no usable zone once an SDK decodes it to a bare instant. `formatted_date` always names the shift's START day, so it is correct for a shift crossing midnight."
                        },
                        "break_records": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/BreakRecord"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Attendance"
        ],
        "summary": "Delete attendance record",
        "description": "Permanently deletes an attendance record. Admin-only in most environments.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Attendance record deleted successfully"
          }
        }
      }
    },
    "/attendance_records/{id}/mark_for_review": {
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "Mark attendance record for review",
        "description": "Flags an attendance record for manager review due to potential issues.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Record marked for review",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "user_id": {
                      "type": "integer"
                    },
                    "shift_id": {
                      "type": "integer"
                    },
                    "business_id": {
                      "type": "integer"
                    },
                    "check_in_time": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "check_out_time": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "on_time",
                        "late",
                        "missed",
                        "completed",
                        "absence_reported",
                        "excused",
                        "no_show"
                      ]
                    },
                    "location_verified": {
                      "type": "boolean",
                      "default": false
                    },
                    "photo_verification_required": {
                      "type": "boolean",
                      "default": false
                    },
                    "photo_verification_failed_reason": {
                      "type": "string",
                      "nullable": true
                    },
                    "is_unknown_device": {
                      "type": "boolean",
                      "default": false
                    },
                    "requires_review": {
                      "type": "boolean",
                      "default": false
                    },
                    "review_reason": {
                      "type": "string",
                      "nullable": true
                    },
                    "reviewed_by_user_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "reviewed_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "excused_by_user_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "excused_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "absence_report_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "notes": {
                      "type": "string",
                      "nullable": true
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "updated_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "user": {
                      "$ref": "#/components/schemas/User"
                    },
                    "shift": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Shift"
                        }
                      ],
                      "nullable": true,
                      "description": "The record's shift. A SUBSET of the Shift schema: id, name, start_time, end_time, formatted_date, formatted_time — plus status, location and location_department when `include=shift_details` is requested. `formatted_date` (\"Wednesday, Aug 05\") and `formatted_time` (\"01:30 PM - 09:30 PM\") are rendered in the SHIFT's own timezone, are byte-identical to what `GET /shifts` returns for the same shift.id, and do not vary with the caller's profile timezone — clients should prefer them over formatting `start_time` locally, which has no usable zone once an SDK decodes it to a bare instant. `formatted_date` always names the shift's START day, so it is correct for a shift crossing midnight."
                    },
                    "break_records": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BreakRecord"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/attendance_records/{id}/approve": {
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "Approve attendance record",
        "description": "Marks an attendance record as approved following review.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Record approved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "user_id": {
                      "type": "integer"
                    },
                    "shift_id": {
                      "type": "integer"
                    },
                    "business_id": {
                      "type": "integer"
                    },
                    "check_in_time": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "check_out_time": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "on_time",
                        "late",
                        "missed",
                        "completed",
                        "absence_reported",
                        "excused",
                        "no_show"
                      ]
                    },
                    "location_verified": {
                      "type": "boolean",
                      "default": false
                    },
                    "photo_verification_required": {
                      "type": "boolean",
                      "default": false
                    },
                    "photo_verification_failed_reason": {
                      "type": "string",
                      "nullable": true
                    },
                    "is_unknown_device": {
                      "type": "boolean",
                      "default": false
                    },
                    "requires_review": {
                      "type": "boolean",
                      "default": false
                    },
                    "review_reason": {
                      "type": "string",
                      "nullable": true
                    },
                    "reviewed_by_user_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "reviewed_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "excused_by_user_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "excused_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "absence_report_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "notes": {
                      "type": "string",
                      "nullable": true
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "updated_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "user": {
                      "$ref": "#/components/schemas/User"
                    },
                    "shift": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Shift"
                        }
                      ],
                      "nullable": true,
                      "description": "The record's shift. A SUBSET of the Shift schema: id, name, start_time, end_time, formatted_date, formatted_time — plus status, location and location_department when `include=shift_details` is requested. `formatted_date` (\"Wednesday, Aug 05\") and `formatted_time` (\"01:30 PM - 09:30 PM\") are rendered in the SHIFT's own timezone, are byte-identical to what `GET /shifts` returns for the same shift.id, and do not vary with the caller's profile timezone — clients should prefer them over formatting `start_time` locally, which has no usable zone once an SDK decodes it to a bare instant. `formatted_date` always names the shift's START day, so it is correct for a shift crossing midnight."
                    },
                    "break_records": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BreakRecord"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/attendance_records/{id}/reject": {
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "Reject attendance record",
        "description": "Marks an attendance record as rejected following review, preserving audit trail.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Record rejected",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "user_id": {
                      "type": "integer"
                    },
                    "shift_id": {
                      "type": "integer"
                    },
                    "business_id": {
                      "type": "integer"
                    },
                    "check_in_time": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "check_out_time": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "on_time",
                        "late",
                        "missed",
                        "completed",
                        "absence_reported",
                        "excused",
                        "no_show"
                      ]
                    },
                    "location_verified": {
                      "type": "boolean",
                      "default": false
                    },
                    "photo_verification_required": {
                      "type": "boolean",
                      "default": false
                    },
                    "photo_verification_failed_reason": {
                      "type": "string",
                      "nullable": true
                    },
                    "is_unknown_device": {
                      "type": "boolean",
                      "default": false
                    },
                    "requires_review": {
                      "type": "boolean",
                      "default": false
                    },
                    "review_reason": {
                      "type": "string",
                      "nullable": true
                    },
                    "reviewed_by_user_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "reviewed_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "excused_by_user_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "excused_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "absence_report_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "notes": {
                      "type": "string",
                      "nullable": true
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "updated_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "user": {
                      "$ref": "#/components/schemas/User"
                    },
                    "shift": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Shift"
                        }
                      ],
                      "nullable": true,
                      "description": "The record's shift. A SUBSET of the Shift schema: id, name, start_time, end_time, formatted_date, formatted_time — plus status, location and location_department when `include=shift_details` is requested. `formatted_date` (\"Wednesday, Aug 05\") and `formatted_time` (\"01:30 PM - 09:30 PM\") are rendered in the SHIFT's own timezone, are byte-identical to what `GET /shifts` returns for the same shift.id, and do not vary with the caller's profile timezone — clients should prefer them over formatting `start_time` locally, which has no usable zone once an SDK decodes it to a bare instant. `formatted_date` always names the shift's START day, so it is correct for a shift crossing midnight."
                    },
                    "break_records": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BreakRecord"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/attendance_records/{id}/adjust": {
      "patch": {
        "tags": [
          "Attendance"
        ],
        "summary": "Adjust attendance record times",
        "description": "Adjusts check-in/check-out times, break duration, and notes for the record.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "attendance_record": {
                    "type": "object",
                    "properties": {
                      "check_in_time": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "check_out_time": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "break_duration": {
                        "type": "integer"
                      },
                      "notes": {
                        "type": "string"
                      },
                      "adjustment_reason": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Record adjusted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "user_id": {
                      "type": "integer"
                    },
                    "shift_id": {
                      "type": "integer"
                    },
                    "business_id": {
                      "type": "integer"
                    },
                    "check_in_time": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "check_out_time": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "on_time",
                        "late",
                        "missed",
                        "completed",
                        "absence_reported",
                        "excused",
                        "no_show"
                      ]
                    },
                    "location_verified": {
                      "type": "boolean",
                      "default": false
                    },
                    "photo_verification_required": {
                      "type": "boolean",
                      "default": false
                    },
                    "photo_verification_failed_reason": {
                      "type": "string",
                      "nullable": true
                    },
                    "is_unknown_device": {
                      "type": "boolean",
                      "default": false
                    },
                    "requires_review": {
                      "type": "boolean",
                      "default": false
                    },
                    "review_reason": {
                      "type": "string",
                      "nullable": true
                    },
                    "reviewed_by_user_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "reviewed_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "excused_by_user_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "excused_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "absence_report_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "notes": {
                      "type": "string",
                      "nullable": true
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "updated_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "user": {
                      "$ref": "#/components/schemas/User"
                    },
                    "shift": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Shift"
                        }
                      ],
                      "nullable": true,
                      "description": "The record's shift. A SUBSET of the Shift schema: id, name, start_time, end_time, formatted_date, formatted_time — plus status, location and location_department when `include=shift_details` is requested. `formatted_date` (\"Wednesday, Aug 05\") and `formatted_time` (\"01:30 PM - 09:30 PM\") are rendered in the SHIFT's own timezone, are byte-identical to what `GET /shifts` returns for the same shift.id, and do not vary with the caller's profile timezone — clients should prefer them over formatting `start_time` locally, which has no usable zone once an SDK decodes it to a bare instant. `formatted_date` always names the shift's START day, so it is correct for a shift crossing midnight."
                    },
                    "break_records": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BreakRecord"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/attendance_records/{id}/check_in": {
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "Check in to shift",
        "description": "Records a check-in event on an existing attendance record with optional verification.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "latitude": {
                    "type": "number",
                    "format": "float",
                    "description": "GPS latitude for location verification"
                  },
                  "longitude": {
                    "type": "number",
                    "format": "float",
                    "description": "GPS longitude for location verification"
                  },
                  "device_fingerprint": {
                    "type": "string",
                    "description": "Device identification for verification"
                  },
                  "photo": {
                    "type": "string",
                    "format": "binary",
                    "description": "Photo for verification (if required)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successfully checked in",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "user_id": {
                      "type": "integer"
                    },
                    "shift_id": {
                      "type": "integer"
                    },
                    "business_id": {
                      "type": "integer"
                    },
                    "check_in_time": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "check_out_time": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "on_time",
                        "late",
                        "missed",
                        "completed",
                        "absence_reported",
                        "excused",
                        "no_show"
                      ]
                    },
                    "location_verified": {
                      "type": "boolean",
                      "default": false
                    },
                    "photo_verification_required": {
                      "type": "boolean",
                      "default": false
                    },
                    "photo_verification_failed_reason": {
                      "type": "string",
                      "nullable": true
                    },
                    "is_unknown_device": {
                      "type": "boolean",
                      "default": false
                    },
                    "requires_review": {
                      "type": "boolean",
                      "default": false
                    },
                    "review_reason": {
                      "type": "string",
                      "nullable": true
                    },
                    "reviewed_by_user_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "reviewed_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "excused_by_user_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "excused_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "absence_report_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "notes": {
                      "type": "string",
                      "nullable": true
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "updated_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "user": {
                      "$ref": "#/components/schemas/User"
                    },
                    "shift": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Shift"
                        }
                      ],
                      "nullable": true,
                      "description": "The record's shift. A SUBSET of the Shift schema: id, name, start_time, end_time, formatted_date, formatted_time — plus status, location and location_department when `include=shift_details` is requested. `formatted_date` (\"Wednesday, Aug 05\") and `formatted_time` (\"01:30 PM - 09:30 PM\") are rendered in the SHIFT's own timezone, are byte-identical to what `GET /shifts` returns for the same shift.id, and do not vary with the caller's profile timezone — clients should prefer them over formatting `start_time` locally, which has no usable zone once an SDK decodes it to a bare instant. `formatted_date` always names the shift's START day, so it is correct for a shift crossing midnight."
                    },
                    "break_records": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BreakRecord"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`error.code` = `kiosk_mode_enabled` — the business runs a shared Time\nClock Kiosk, so clocking in from a personal device is turned off.\nApplies to the caller's own record only; carries\n`error.details.time_clock_kiosk_enabled: true`. Not retryable — the\nemployee must clock in at the kiosk.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "422": {
            "description": "Check-in validation failed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/attendance_records/{id}/start_break": {
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "Start a break",
        "description": "Starts a break for the current attendance session, optionally setting a break type.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "break_type_id": {
                    "type": "integer",
                    "description": "Type of break (optional, uses default if not provided)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Break started successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "user_id": {
                      "type": "integer"
                    },
                    "shift_id": {
                      "type": "integer"
                    },
                    "business_id": {
                      "type": "integer"
                    },
                    "check_in_time": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "check_out_time": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "on_time",
                        "late",
                        "missed",
                        "completed",
                        "absence_reported",
                        "excused",
                        "no_show"
                      ]
                    },
                    "location_verified": {
                      "type": "boolean",
                      "default": false
                    },
                    "photo_verification_required": {
                      "type": "boolean",
                      "default": false
                    },
                    "photo_verification_failed_reason": {
                      "type": "string",
                      "nullable": true
                    },
                    "is_unknown_device": {
                      "type": "boolean",
                      "default": false
                    },
                    "requires_review": {
                      "type": "boolean",
                      "default": false
                    },
                    "review_reason": {
                      "type": "string",
                      "nullable": true
                    },
                    "reviewed_by_user_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "reviewed_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "excused_by_user_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "excused_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "absence_report_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "notes": {
                      "type": "string",
                      "nullable": true
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "updated_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "user": {
                      "$ref": "#/components/schemas/User"
                    },
                    "shift": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Shift"
                        }
                      ],
                      "nullable": true,
                      "description": "The record's shift. A SUBSET of the Shift schema: id, name, start_time, end_time, formatted_date, formatted_time — plus status, location and location_department when `include=shift_details` is requested. `formatted_date` (\"Wednesday, Aug 05\") and `formatted_time` (\"01:30 PM - 09:30 PM\") are rendered in the SHIFT's own timezone, are byte-identical to what `GET /shifts` returns for the same shift.id, and do not vary with the caller's profile timezone — clients should prefer them over formatting `start_time` locally, which has no usable zone once an SDK decodes it to a bare instant. `formatted_date` always names the shift's START day, so it is correct for a shift crossing midnight."
                    },
                    "break_records": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BreakRecord"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`error.code` = `kiosk_mode_enabled` — the business runs a shared Time\nClock Kiosk, so breaks are started at the kiosk, not from a personal\ndevice. Applies to the caller's own record only; carries\n`error.details.time_clock_kiosk_enabled: true`. Not retryable.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "422": {
            "description": "Break start validation failed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/attendance_records/{id}/end_break": {
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "End current break",
        "description": "Ends the active break for the attendance record and updates totals.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Break ended successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "user_id": {
                      "type": "integer"
                    },
                    "shift_id": {
                      "type": "integer"
                    },
                    "business_id": {
                      "type": "integer"
                    },
                    "check_in_time": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "check_out_time": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "on_time",
                        "late",
                        "missed",
                        "completed",
                        "absence_reported",
                        "excused",
                        "no_show"
                      ]
                    },
                    "location_verified": {
                      "type": "boolean",
                      "default": false
                    },
                    "photo_verification_required": {
                      "type": "boolean",
                      "default": false
                    },
                    "photo_verification_failed_reason": {
                      "type": "string",
                      "nullable": true
                    },
                    "is_unknown_device": {
                      "type": "boolean",
                      "default": false
                    },
                    "requires_review": {
                      "type": "boolean",
                      "default": false
                    },
                    "review_reason": {
                      "type": "string",
                      "nullable": true
                    },
                    "reviewed_by_user_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "reviewed_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "excused_by_user_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "excused_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "absence_report_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "notes": {
                      "type": "string",
                      "nullable": true
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "updated_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "user": {
                      "$ref": "#/components/schemas/User"
                    },
                    "shift": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Shift"
                        }
                      ],
                      "nullable": true,
                      "description": "The record's shift. A SUBSET of the Shift schema: id, name, start_time, end_time, formatted_date, formatted_time — plus status, location and location_department when `include=shift_details` is requested. `formatted_date` (\"Wednesday, Aug 05\") and `formatted_time` (\"01:30 PM - 09:30 PM\") are rendered in the SHIFT's own timezone, are byte-identical to what `GET /shifts` returns for the same shift.id, and do not vary with the caller's profile timezone — clients should prefer them over formatting `start_time` locally, which has no usable zone once an SDK decodes it to a bare instant. `formatted_date` always names the shift's START day, so it is correct for a shift crossing midnight."
                    },
                    "break_records": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BreakRecord"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`error.code` = `kiosk_mode_enabled` — the business runs a shared Time\nClock Kiosk, so breaks are ended at the kiosk, not from a personal\ndevice. Applies to the caller's own record only; carries\n`error.details.time_clock_kiosk_enabled: true`. Not retryable.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "422": {
            "description": "No active break to end",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/attendance_records/dashboard": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "Get attendance dashboard statistics",
        "description": "Provides summarized counts and KPIs for the attendance dashboard.",
        "responses": {
          "200": {
            "description": "Dashboard statistics",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "total_records": {
                      "type": "integer"
                    },
                    "pending_review": {
                      "type": "integer"
                    },
                    "missing_check_out": {
                      "type": "integer"
                    },
                    "today_records": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/attendance_records/analytics": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "Get attendance analytics",
        "description": "Returns aggregate analytics for attendance across dimensions such as status and location.",
        "responses": {
          "200": {
            "description": "Attendance analytics data",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "by_status": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "integer"
                      }
                    },
                    "by_department": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "integer"
                      }
                    },
                    "by_location": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "integer"
                      }
                    },
                    "average_duration": {
                      "type": "number",
                      "format": "float"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/attendance_records/history": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "Get attendance history",
        "description": "Returns a recent history list of attendance records with a configurable limit.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Attendance history",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "integer"
                      },
                      "user_id": {
                        "type": "integer"
                      },
                      "shift_id": {
                        "type": "integer"
                      },
                      "business_id": {
                        "type": "integer"
                      },
                      "check_in_time": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                      },
                      "check_out_time": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                      },
                      "status": {
                        "type": "string",
                        "enum": [
                          "pending",
                          "on_time",
                          "late",
                          "missed",
                          "completed",
                          "absence_reported",
                          "excused",
                          "no_show"
                        ]
                      },
                      "location_verified": {
                        "type": "boolean",
                        "default": false
                      },
                      "photo_verification_required": {
                        "type": "boolean",
                        "default": false
                      },
                      "photo_verification_failed_reason": {
                        "type": "string",
                        "nullable": true
                      },
                      "is_unknown_device": {
                        "type": "boolean",
                        "default": false
                      },
                      "requires_review": {
                        "type": "boolean",
                        "default": false
                      },
                      "review_reason": {
                        "type": "string",
                        "nullable": true
                      },
                      "reviewed_by_user_id": {
                        "type": "integer",
                        "nullable": true
                      },
                      "reviewed_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                      },
                      "excused_by_user_id": {
                        "type": "integer",
                        "nullable": true
                      },
                      "excused_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                      },
                      "absence_report_id": {
                        "type": "integer",
                        "nullable": true
                      },
                      "notes": {
                        "type": "string",
                        "nullable": true
                      },
                      "created_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "updated_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "user": {
                        "$ref": "#/components/schemas/User"
                      },
                      "shift": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/Shift"
                          }
                        ],
                        "nullable": true,
                        "description": "The record's shift. A SUBSET of the Shift schema: id, name, start_time, end_time, formatted_date, formatted_time — plus status, location and location_department when `include=shift_details` is requested. `formatted_date` (\"Wednesday, Aug 05\") and `formatted_time` (\"01:30 PM - 09:30 PM\") are rendered in the SHIFT's own timezone, are byte-identical to what `GET /shifts` returns for the same shift.id, and do not vary with the caller's profile timezone — clients should prefer them over formatting `start_time` locally, which has no usable zone once an SDK decodes it to a bare instant. `formatted_date` always names the shift's START day, so it is correct for a shift crossing midnight."
                      },
                      "break_records": {
                        "type": "array",
                        "items": {
                          "$ref": "#/components/schemas/BreakRecord"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/attendance_records/missing_check_out": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "Get records missing check-out",
        "description": "Lists attendance records that are missing a check-out and may require follow-up.",
        "responses": {
          "200": {
            "description": "Records missing check-out",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "integer"
                      },
                      "user_id": {
                        "type": "integer"
                      },
                      "shift_id": {
                        "type": "integer"
                      },
                      "business_id": {
                        "type": "integer"
                      },
                      "check_in_time": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                      },
                      "check_out_time": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                      },
                      "status": {
                        "type": "string",
                        "enum": [
                          "pending",
                          "on_time",
                          "late",
                          "missed",
                          "completed",
                          "absence_reported",
                          "excused",
                          "no_show"
                        ]
                      },
                      "location_verified": {
                        "type": "boolean",
                        "default": false
                      },
                      "photo_verification_required": {
                        "type": "boolean",
                        "default": false
                      },
                      "photo_verification_failed_reason": {
                        "type": "string",
                        "nullable": true
                      },
                      "is_unknown_device": {
                        "type": "boolean",
                        "default": false
                      },
                      "requires_review": {
                        "type": "boolean",
                        "default": false
                      },
                      "review_reason": {
                        "type": "string",
                        "nullable": true
                      },
                      "reviewed_by_user_id": {
                        "type": "integer",
                        "nullable": true
                      },
                      "reviewed_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                      },
                      "excused_by_user_id": {
                        "type": "integer",
                        "nullable": true
                      },
                      "excused_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                      },
                      "absence_report_id": {
                        "type": "integer",
                        "nullable": true
                      },
                      "notes": {
                        "type": "string",
                        "nullable": true
                      },
                      "created_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "updated_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "user": {
                        "$ref": "#/components/schemas/User"
                      },
                      "shift": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/Shift"
                          }
                        ],
                        "nullable": true,
                        "description": "The record's shift. A SUBSET of the Shift schema: id, name, start_time, end_time, formatted_date, formatted_time — plus status, location and location_department when `include=shift_details` is requested. `formatted_date` (\"Wednesday, Aug 05\") and `formatted_time` (\"01:30 PM - 09:30 PM\") are rendered in the SHIFT's own timezone, are byte-identical to what `GET /shifts` returns for the same shift.id, and do not vary with the caller's profile timezone — clients should prefer them over formatting `start_time` locally, which has no usable zone once an SDK decodes it to a bare instant. `formatted_date` always names the shift's START day, so it is correct for a shift crossing midnight."
                      },
                      "break_records": {
                        "type": "array",
                        "items": {
                          "$ref": "#/components/schemas/BreakRecord"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/attendance_records/requires_review": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "Get records requiring review",
        "description": "Lists attendance records flagged for review by automated or manual checks.",
        "responses": {
          "200": {
            "description": "Records requiring review",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "integer"
                      },
                      "user_id": {
                        "type": "integer"
                      },
                      "shift_id": {
                        "type": "integer"
                      },
                      "business_id": {
                        "type": "integer"
                      },
                      "check_in_time": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                      },
                      "check_out_time": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                      },
                      "status": {
                        "type": "string",
                        "enum": [
                          "pending",
                          "on_time",
                          "late",
                          "missed",
                          "completed",
                          "absence_reported",
                          "excused",
                          "no_show"
                        ]
                      },
                      "location_verified": {
                        "type": "boolean",
                        "default": false
                      },
                      "photo_verification_required": {
                        "type": "boolean",
                        "default": false
                      },
                      "photo_verification_failed_reason": {
                        "type": "string",
                        "nullable": true
                      },
                      "is_unknown_device": {
                        "type": "boolean",
                        "default": false
                      },
                      "requires_review": {
                        "type": "boolean",
                        "default": false
                      },
                      "review_reason": {
                        "type": "string",
                        "nullable": true
                      },
                      "reviewed_by_user_id": {
                        "type": "integer",
                        "nullable": true
                      },
                      "reviewed_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                      },
                      "excused_by_user_id": {
                        "type": "integer",
                        "nullable": true
                      },
                      "excused_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                      },
                      "absence_report_id": {
                        "type": "integer",
                        "nullable": true
                      },
                      "notes": {
                        "type": "string",
                        "nullable": true
                      },
                      "created_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "updated_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "user": {
                        "$ref": "#/components/schemas/User"
                      },
                      "shift": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/Shift"
                          }
                        ],
                        "nullable": true,
                        "description": "The record's shift. A SUBSET of the Shift schema: id, name, start_time, end_time, formatted_date, formatted_time — plus status, location and location_department when `include=shift_details` is requested. `formatted_date` (\"Wednesday, Aug 05\") and `formatted_time` (\"01:30 PM - 09:30 PM\") are rendered in the SHIFT's own timezone, are byte-identical to what `GET /shifts` returns for the same shift.id, and do not vary with the caller's profile timezone — clients should prefer them over formatting `start_time` locally, which has no usable zone once an SDK decodes it to a bare instant. `formatted_date` always names the shift's START day, so it is correct for a shift crossing midnight."
                      },
                      "break_records": {
                        "type": "array",
                        "items": {
                          "$ref": "#/components/schemas/BreakRecord"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/attendance_records/auto_check_out": {
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "Auto check-out stale records",
        "description": "Automatically check out records that have been open for more than 24 hours",
        "responses": {
          "200": {
            "description": "Auto check-out completed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "results": {
                      "type": "object",
                      "properties": {
                        "success": {
                          "type": "array",
                          "items": {
                            "type": "integer"
                          }
                        },
                        "failed": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/absence_reasons": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "The tenant's absence reason codes",
        "description": "The configured reason list behind the \"Report Delay or Absence\" form —\nthe same set, in the same order, that the web picker is built from. Read\nthis instead of shipping a built-in list: the six defaults are only a\nSEED, and a tenant may rename, retire or add codes at any time from\n/admin/absence_reason_codes.\n\nNot paginated — the whole set is one select's worth of options.\n\nRead-only. Reason codes are admin configuration; there is no API to\ncreate or edit one.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "include_inactive",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "Include RETIRED codes. Default false, which is what a\nnew-report picker wants — nothing new may be filed under a retired\nreason.\n\nPass true only to NAME a reason on a report that already exists: a\ncode is retired with a flag rather than deleted, so a report filed\nbefore the retirement still points at one. Every row carries\n`active`, so a client asking for the full set can still keep retired\ncodes out of the picker.\n"
          }
        ],
        "responses": {
          "200": {
            "description": "Reason codes for the caller's business",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "absence_reasons": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AbsenceReason"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "include_inactive": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Time & Attendance is not enabled for the business or is not accessible to this user, or the token lacks `read:attendance` / `read:own_attendance`.\n"
          }
        }
      }
    },
    "/shift_marketplace/listings": {
      "get": {
        "tags": [
          "Shift Marketplace"
        ],
        "summary": "List Shift Marketplace Listings",
        "description": "Returns a list of shift marketplace listings with filtering by listing type, status, price, and location.\nSupports pickup-only, trade-only, and both listing types.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "listing_type",
            "in": "query",
            "description": "Filter by listing type",
            "schema": {
              "type": "string",
              "enum": [
                "pickup",
                "trade_only",
                "both"
              ]
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by listing status",
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "filled",
                "closed",
                "cancelled"
              ]
            }
          },
          {
            "name": "min_price",
            "in": "query",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "max_price",
            "in": "query",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "location_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of marketplace listings",
            "headers": {
              "X-Total-Count": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Total-Pages": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "A marketplace listing for shifts (pickup, trade_only, or both)",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 1
                          },
                          "listing_type": {
                            "type": "string",
                            "enum": [
                              "pickup",
                              "trade_only",
                              "both"
                            ],
                            "example": "pickup",
                            "description": "Type of listing - pickup (direct claim), trade_only (requires application), or both"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "open",
                              "filled",
                              "closed",
                              "cancelled"
                            ],
                            "example": "open"
                          },
                          "price": {
                            "type": "number",
                            "nullable": true,
                            "example": 0.01
                          },
                          "currency": {
                            "type": "string",
                            "example": "USD"
                          },
                          "notes": {
                            "type": "string",
                            "nullable": true,
                            "example": "Need someone to cover this shift"
                          },
                          "urgent": {
                            "type": "boolean",
                            "description": "Whether this listing is marked as urgent",
                            "example": false
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "claimed_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "shift": {
                            "type": "object",
                            "nullable": true,
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string"
                              },
                              "start_time": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "end_time": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "date": {
                                "type": "string",
                                "format": "date"
                              },
                              "formatted_date": {
                                "type": "string"
                              },
                              "formatted_time": {
                                "type": "string"
                              },
                              "location": {
                                "type": "object",
                                "nullable": true,
                                "properties": {
                                  "id": {
                                    "type": "integer"
                                  },
                                  "name": {
                                    "type": "string"
                                  }
                                }
                              }
                            }
                          },
                          "created_by": {
                            "type": "object",
                            "nullable": true,
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string"
                              },
                              "email": {
                                "type": "string"
                              },
                              "avatar_url": {
                                "type": "string",
                                "nullable": true,
                                "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                              },
                              "avatar_thumbnail_url": {
                                "type": "string",
                                "nullable": true,
                                "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                              }
                            }
                          },
                          "claimed_by": {
                            "type": "object",
                            "nullable": true,
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string"
                              },
                              "email": {
                                "type": "string"
                              },
                              "avatar_url": {
                                "type": "string",
                                "nullable": true,
                                "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                              },
                              "avatar_thumbnail_url": {
                                "type": "string",
                                "nullable": true,
                                "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                              }
                            }
                          },
                          "can_claim": {
                            "type": "boolean",
                            "description": "Whether current user can claim this listing"
                          },
                          "accepts_applications": {
                            "type": "boolean",
                            "description": "Whether this listing accepts applications (trade_only or both)"
                          },
                          "is_trade_only": {
                            "type": "boolean",
                            "description": "Whether this is a trade-only listing"
                          },
                          "has_applied": {
                            "type": "boolean",
                            "description": "Whether current user has applied to this listing"
                          },
                          "application_status": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "pending",
                              "accepted",
                              "rejected",
                              "withdrawn"
                            ],
                            "description": "Current user's application status (if applied)"
                          },
                          "application_id": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Current user's application ID (if applied)"
                          },
                          "is_owner": {
                            "type": "boolean",
                            "description": "Whether current user owns this listing"
                          },
                          "can_edit": {
                            "type": "boolean"
                          },
                          "can_delete": {
                            "type": "boolean"
                          },
                          "applications": {
                            "type": "array",
                            "description": "Owner-only application summaries for this listing",
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "type": "integer"
                                },
                                "status": {
                                  "type": "string",
                                  "enum": [
                                    "pending",
                                    "accepted",
                                    "rejected",
                                    "withdrawn"
                                  ]
                                },
                                "notes": {
                                  "type": "string",
                                  "nullable": true
                                },
                                "created_at": {
                                  "type": "string",
                                  "format": "date-time"
                                },
                                "offered_shift": {
                                  "type": "object",
                                  "nullable": true,
                                  "properties": {
                                    "id": {
                                      "type": "integer"
                                    },
                                    "name": {
                                      "type": "string"
                                    },
                                    "start_time": {
                                      "type": "string",
                                      "format": "date-time"
                                    },
                                    "end_time": {
                                      "type": "string",
                                      "format": "date-time"
                                    },
                                    "formatted_date": {
                                      "type": "string"
                                    },
                                    "formatted_time": {
                                      "type": "string"
                                    },
                                    "location": {
                                      "type": "object",
                                      "nullable": true,
                                      "properties": {
                                        "id": {
                                          "type": "integer"
                                        },
                                        "name": {
                                          "type": "string"
                                        }
                                      }
                                    }
                                  }
                                },
                                "applicant": {
                                  "type": "object",
                                  "nullable": true,
                                  "properties": {
                                    "id": {
                                      "type": "integer"
                                    },
                                    "name": {
                                      "type": "string"
                                    },
                                    "avatar_url": {
                                      "type": "string",
                                      "nullable": true
                                    },
                                    "avatar_thumbnail_url": {
                                      "type": "string",
                                      "nullable": true
                                    }
                                  }
                                }
                              }
                            }
                          },
                          "applications_count": {
                            "type": "integer",
                            "description": "Total applications (owner only)"
                          },
                          "pending_applications_count": {
                            "type": "integer",
                            "description": "Pending applications count (owner only)"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 6
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Items per page",
                          "example": 25
                        }
                      },
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required or token invalid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient permissions to access this resource",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Shift Marketplace"
        ],
        "summary": "Create Marketplace Listing",
        "description": "Create a new shift marketplace listing (pickup, trade_only, or both)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "listing"
                ],
                "properties": {
                  "listing": {
                    "type": "object",
                    "required": [
                      "shift_id",
                      "listing_type"
                    ],
                    "properties": {
                      "shift_id": {
                        "type": "integer",
                        "example": 123
                      },
                      "listing_type": {
                        "type": "string",
                        "enum": [
                          "pickup",
                          "trade_only",
                          "both"
                        ],
                        "example": "pickup",
                        "description": "Type of listing - pickup (direct claim), trade_only (requires application), or both"
                      },
                      "price": {
                        "type": "number",
                        "example": 0.01,
                        "description": "Optional price/incentive for pickup"
                      },
                      "currency": {
                        "type": "string",
                        "example": "USD",
                        "default": "USD"
                      },
                      "notes": {
                        "type": "string",
                        "example": "Need someone to cover this shift",
                        "description": "Optional notes about the listing"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Listing created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "listing": {
                      "type": "object",
                      "description": "A marketplace listing for shifts (pickup, trade_only, or both)",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 1
                        },
                        "listing_type": {
                          "type": "string",
                          "enum": [
                            "pickup",
                            "trade_only",
                            "both"
                          ],
                          "example": "pickup",
                          "description": "Type of listing - pickup (direct claim), trade_only (requires application), or both"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "open",
                            "filled",
                            "closed",
                            "cancelled"
                          ],
                          "example": "open"
                        },
                        "price": {
                          "type": "number",
                          "nullable": true,
                          "example": 0.01
                        },
                        "currency": {
                          "type": "string",
                          "example": "USD"
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true,
                          "example": "Need someone to cover this shift"
                        },
                        "urgent": {
                          "type": "boolean",
                          "description": "Whether this listing is marked as urgent",
                          "example": false
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "claimed_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "shift": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "start_time": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "end_time": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "date": {
                              "type": "string",
                              "format": "date"
                            },
                            "formatted_date": {
                              "type": "string"
                            },
                            "formatted_time": {
                              "type": "string"
                            },
                            "location": {
                              "type": "object",
                              "nullable": true,
                              "properties": {
                                "id": {
                                  "type": "integer"
                                },
                                "name": {
                                  "type": "string"
                                }
                              }
                            }
                          }
                        },
                        "created_by": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "email": {
                              "type": "string"
                            },
                            "avatar_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                            },
                            "avatar_thumbnail_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                            }
                          }
                        },
                        "claimed_by": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "email": {
                              "type": "string"
                            },
                            "avatar_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                            },
                            "avatar_thumbnail_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                            }
                          }
                        },
                        "can_claim": {
                          "type": "boolean",
                          "description": "Whether current user can claim this listing"
                        },
                        "accepts_applications": {
                          "type": "boolean",
                          "description": "Whether this listing accepts applications (trade_only or both)"
                        },
                        "is_trade_only": {
                          "type": "boolean",
                          "description": "Whether this is a trade-only listing"
                        },
                        "has_applied": {
                          "type": "boolean",
                          "description": "Whether current user has applied to this listing"
                        },
                        "application_status": {
                          "type": "string",
                          "nullable": true,
                          "enum": [
                            "pending",
                            "accepted",
                            "rejected",
                            "withdrawn"
                          ],
                          "description": "Current user's application status (if applied)"
                        },
                        "application_id": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Current user's application ID (if applied)"
                        },
                        "is_owner": {
                          "type": "boolean",
                          "description": "Whether current user owns this listing"
                        },
                        "can_edit": {
                          "type": "boolean"
                        },
                        "can_delete": {
                          "type": "boolean"
                        },
                        "applications": {
                          "type": "array",
                          "description": "Owner-only application summaries for this listing",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "status": {
                                "type": "string",
                                "enum": [
                                  "pending",
                                  "accepted",
                                  "rejected",
                                  "withdrawn"
                                ]
                              },
                              "notes": {
                                "type": "string",
                                "nullable": true
                              },
                              "created_at": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "offered_shift": {
                                "type": "object",
                                "nullable": true,
                                "properties": {
                                  "id": {
                                    "type": "integer"
                                  },
                                  "name": {
                                    "type": "string"
                                  },
                                  "start_time": {
                                    "type": "string",
                                    "format": "date-time"
                                  },
                                  "end_time": {
                                    "type": "string",
                                    "format": "date-time"
                                  },
                                  "formatted_date": {
                                    "type": "string"
                                  },
                                  "formatted_time": {
                                    "type": "string"
                                  },
                                  "location": {
                                    "type": "object",
                                    "nullable": true,
                                    "properties": {
                                      "id": {
                                        "type": "integer"
                                      },
                                      "name": {
                                        "type": "string"
                                      }
                                    }
                                  }
                                }
                              },
                              "applicant": {
                                "type": "object",
                                "nullable": true,
                                "properties": {
                                  "id": {
                                    "type": "integer"
                                  },
                                  "name": {
                                    "type": "string"
                                  },
                                  "avatar_url": {
                                    "type": "string",
                                    "nullable": true
                                  },
                                  "avatar_thumbnail_url": {
                                    "type": "string",
                                    "nullable": true
                                  }
                                }
                              }
                            }
                          }
                        },
                        "applications_count": {
                          "type": "integer",
                          "description": "Total applications (owner only)"
                        },
                        "pending_applications_count": {
                          "type": "integer",
                          "description": "Pending applications count (owner only)"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required or token invalid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          }
        }
      }
    },
    "/shift_marketplace/listings/{id}": {
      "get": {
        "tags": [
          "Shift Marketplace"
        ],
        "summary": "Get Listing Details",
        "description": "Returns detailed information about a specific marketplace listing including:\n- Basic listing information (type, status, price)\n- Associated shift details\n- Current user's application status (has_applied, application_status, application_id)\n- Applications list if user is the listing owner\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Listing details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "listing": {
                      "type": "object",
                      "description": "A marketplace listing for shifts (pickup, trade_only, or both)",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 1
                        },
                        "listing_type": {
                          "type": "string",
                          "enum": [
                            "pickup",
                            "trade_only",
                            "both"
                          ],
                          "example": "pickup",
                          "description": "Type of listing - pickup (direct claim), trade_only (requires application), or both"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "open",
                            "filled",
                            "closed",
                            "cancelled"
                          ],
                          "example": "open"
                        },
                        "price": {
                          "type": "number",
                          "nullable": true,
                          "example": 0.01
                        },
                        "currency": {
                          "type": "string",
                          "example": "USD"
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true,
                          "example": "Need someone to cover this shift"
                        },
                        "urgent": {
                          "type": "boolean",
                          "description": "Whether this listing is marked as urgent",
                          "example": false
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "claimed_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "shift": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "start_time": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "end_time": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "date": {
                              "type": "string",
                              "format": "date"
                            },
                            "formatted_date": {
                              "type": "string"
                            },
                            "formatted_time": {
                              "type": "string"
                            },
                            "location": {
                              "type": "object",
                              "nullable": true,
                              "properties": {
                                "id": {
                                  "type": "integer"
                                },
                                "name": {
                                  "type": "string"
                                }
                              }
                            }
                          }
                        },
                        "created_by": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "email": {
                              "type": "string"
                            },
                            "avatar_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                            },
                            "avatar_thumbnail_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                            }
                          }
                        },
                        "claimed_by": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "email": {
                              "type": "string"
                            },
                            "avatar_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                            },
                            "avatar_thumbnail_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                            }
                          }
                        },
                        "can_claim": {
                          "type": "boolean",
                          "description": "Whether current user can claim this listing"
                        },
                        "accepts_applications": {
                          "type": "boolean",
                          "description": "Whether this listing accepts applications (trade_only or both)"
                        },
                        "is_trade_only": {
                          "type": "boolean",
                          "description": "Whether this is a trade-only listing"
                        },
                        "has_applied": {
                          "type": "boolean",
                          "description": "Whether current user has applied to this listing"
                        },
                        "application_status": {
                          "type": "string",
                          "nullable": true,
                          "enum": [
                            "pending",
                            "accepted",
                            "rejected",
                            "withdrawn"
                          ],
                          "description": "Current user's application status (if applied)"
                        },
                        "application_id": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Current user's application ID (if applied)"
                        },
                        "is_owner": {
                          "type": "boolean",
                          "description": "Whether current user owns this listing"
                        },
                        "can_edit": {
                          "type": "boolean"
                        },
                        "can_delete": {
                          "type": "boolean"
                        },
                        "applications": {
                          "type": "array",
                          "description": "Owner-only application summaries for this listing",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "status": {
                                "type": "string",
                                "enum": [
                                  "pending",
                                  "accepted",
                                  "rejected",
                                  "withdrawn"
                                ]
                              },
                              "notes": {
                                "type": "string",
                                "nullable": true
                              },
                              "created_at": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "offered_shift": {
                                "type": "object",
                                "nullable": true,
                                "properties": {
                                  "id": {
                                    "type": "integer"
                                  },
                                  "name": {
                                    "type": "string"
                                  },
                                  "start_time": {
                                    "type": "string",
                                    "format": "date-time"
                                  },
                                  "end_time": {
                                    "type": "string",
                                    "format": "date-time"
                                  },
                                  "formatted_date": {
                                    "type": "string"
                                  },
                                  "formatted_time": {
                                    "type": "string"
                                  },
                                  "location": {
                                    "type": "object",
                                    "nullable": true,
                                    "properties": {
                                      "id": {
                                        "type": "integer"
                                      },
                                      "name": {
                                        "type": "string"
                                      }
                                    }
                                  }
                                }
                              },
                              "applicant": {
                                "type": "object",
                                "nullable": true,
                                "properties": {
                                  "id": {
                                    "type": "integer"
                                  },
                                  "name": {
                                    "type": "string"
                                  },
                                  "avatar_url": {
                                    "type": "string",
                                    "nullable": true
                                  },
                                  "avatar_thumbnail_url": {
                                    "type": "string",
                                    "nullable": true
                                  }
                                }
                              }
                            }
                          }
                        },
                        "applications_count": {
                          "type": "integer",
                          "description": "Total applications (owner only)"
                        },
                        "pending_applications_count": {
                          "type": "integer",
                          "description": "Pending applications count (owner only)"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required or token invalid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Resource not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Shift Marketplace"
        ],
        "summary": "Update Listing",
        "description": "Update a marketplace listing (owner only)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "listing": {
                    "type": "object",
                    "properties": {
                      "listing_type": {
                        "type": "string",
                        "enum": [
                          "pickup",
                          "trade_only",
                          "both"
                        ]
                      },
                      "price": {
                        "type": "number"
                      },
                      "notes": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Listing updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "listing": {
                      "type": "object",
                      "description": "A marketplace listing for shifts (pickup, trade_only, or both)",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 1
                        },
                        "listing_type": {
                          "type": "string",
                          "enum": [
                            "pickup",
                            "trade_only",
                            "both"
                          ],
                          "example": "pickup",
                          "description": "Type of listing - pickup (direct claim), trade_only (requires application), or both"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "open",
                            "filled",
                            "closed",
                            "cancelled"
                          ],
                          "example": "open"
                        },
                        "price": {
                          "type": "number",
                          "nullable": true,
                          "example": 0.01
                        },
                        "currency": {
                          "type": "string",
                          "example": "USD"
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true,
                          "example": "Need someone to cover this shift"
                        },
                        "urgent": {
                          "type": "boolean",
                          "description": "Whether this listing is marked as urgent",
                          "example": false
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "claimed_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "shift": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "start_time": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "end_time": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "date": {
                              "type": "string",
                              "format": "date"
                            },
                            "formatted_date": {
                              "type": "string"
                            },
                            "formatted_time": {
                              "type": "string"
                            },
                            "location": {
                              "type": "object",
                              "nullable": true,
                              "properties": {
                                "id": {
                                  "type": "integer"
                                },
                                "name": {
                                  "type": "string"
                                }
                              }
                            }
                          }
                        },
                        "created_by": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "email": {
                              "type": "string"
                            },
                            "avatar_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                            },
                            "avatar_thumbnail_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                            }
                          }
                        },
                        "claimed_by": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "email": {
                              "type": "string"
                            },
                            "avatar_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                            },
                            "avatar_thumbnail_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                            }
                          }
                        },
                        "can_claim": {
                          "type": "boolean",
                          "description": "Whether current user can claim this listing"
                        },
                        "accepts_applications": {
                          "type": "boolean",
                          "description": "Whether this listing accepts applications (trade_only or both)"
                        },
                        "is_trade_only": {
                          "type": "boolean",
                          "description": "Whether this is a trade-only listing"
                        },
                        "has_applied": {
                          "type": "boolean",
                          "description": "Whether current user has applied to this listing"
                        },
                        "application_status": {
                          "type": "string",
                          "nullable": true,
                          "enum": [
                            "pending",
                            "accepted",
                            "rejected",
                            "withdrawn"
                          ],
                          "description": "Current user's application status (if applied)"
                        },
                        "application_id": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Current user's application ID (if applied)"
                        },
                        "is_owner": {
                          "type": "boolean",
                          "description": "Whether current user owns this listing"
                        },
                        "can_edit": {
                          "type": "boolean"
                        },
                        "can_delete": {
                          "type": "boolean"
                        },
                        "applications": {
                          "type": "array",
                          "description": "Owner-only application summaries for this listing",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "status": {
                                "type": "string",
                                "enum": [
                                  "pending",
                                  "accepted",
                                  "rejected",
                                  "withdrawn"
                                ]
                              },
                              "notes": {
                                "type": "string",
                                "nullable": true
                              },
                              "created_at": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "offered_shift": {
                                "type": "object",
                                "nullable": true,
                                "properties": {
                                  "id": {
                                    "type": "integer"
                                  },
                                  "name": {
                                    "type": "string"
                                  },
                                  "start_time": {
                                    "type": "string",
                                    "format": "date-time"
                                  },
                                  "end_time": {
                                    "type": "string",
                                    "format": "date-time"
                                  },
                                  "formatted_date": {
                                    "type": "string"
                                  },
                                  "formatted_time": {
                                    "type": "string"
                                  },
                                  "location": {
                                    "type": "object",
                                    "nullable": true,
                                    "properties": {
                                      "id": {
                                        "type": "integer"
                                      },
                                      "name": {
                                        "type": "string"
                                      }
                                    }
                                  }
                                }
                              },
                              "applicant": {
                                "type": "object",
                                "nullable": true,
                                "properties": {
                                  "id": {
                                    "type": "integer"
                                  },
                                  "name": {
                                    "type": "string"
                                  },
                                  "avatar_url": {
                                    "type": "string",
                                    "nullable": true
                                  },
                                  "avatar_thumbnail_url": {
                                    "type": "string",
                                    "nullable": true
                                  }
                                }
                              }
                            }
                          }
                        },
                        "applications_count": {
                          "type": "integer",
                          "description": "Total applications (owner only)"
                        },
                        "pending_applications_count": {
                          "type": "integer",
                          "description": "Pending applications count (owner only)"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Insufficient permissions to access this resource",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Shift Marketplace"
        ],
        "summary": "Delete Listing",
        "description": "Delete a marketplace listing (owner only)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Listing deleted"
          },
          "403": {
            "description": "Insufficient permissions to access this resource",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/shift_marketplace/listings/{id}/claim": {
      "post": {
        "tags": [
          "Shift Marketplace"
        ],
        "summary": "Claim Listing",
        "description": "Claim a pickup or both listing. Trade-only listings cannot be claimed directly.\nUse the applications endpoint instead for trade-only listings.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Listing claimed successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "listing": {
                      "type": "object",
                      "description": "A marketplace listing for shifts (pickup, trade_only, or both)",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 1
                        },
                        "listing_type": {
                          "type": "string",
                          "enum": [
                            "pickup",
                            "trade_only",
                            "both"
                          ],
                          "example": "pickup",
                          "description": "Type of listing - pickup (direct claim), trade_only (requires application), or both"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "open",
                            "filled",
                            "closed",
                            "cancelled"
                          ],
                          "example": "open"
                        },
                        "price": {
                          "type": "number",
                          "nullable": true,
                          "example": 0.01
                        },
                        "currency": {
                          "type": "string",
                          "example": "USD"
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true,
                          "example": "Need someone to cover this shift"
                        },
                        "urgent": {
                          "type": "boolean",
                          "description": "Whether this listing is marked as urgent",
                          "example": false
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "claimed_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "shift": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "start_time": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "end_time": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "date": {
                              "type": "string",
                              "format": "date"
                            },
                            "formatted_date": {
                              "type": "string"
                            },
                            "formatted_time": {
                              "type": "string"
                            },
                            "location": {
                              "type": "object",
                              "nullable": true,
                              "properties": {
                                "id": {
                                  "type": "integer"
                                },
                                "name": {
                                  "type": "string"
                                }
                              }
                            }
                          }
                        },
                        "created_by": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "email": {
                              "type": "string"
                            },
                            "avatar_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                            },
                            "avatar_thumbnail_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                            }
                          }
                        },
                        "claimed_by": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "email": {
                              "type": "string"
                            },
                            "avatar_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                            },
                            "avatar_thumbnail_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                            }
                          }
                        },
                        "can_claim": {
                          "type": "boolean",
                          "description": "Whether current user can claim this listing"
                        },
                        "accepts_applications": {
                          "type": "boolean",
                          "description": "Whether this listing accepts applications (trade_only or both)"
                        },
                        "is_trade_only": {
                          "type": "boolean",
                          "description": "Whether this is a trade-only listing"
                        },
                        "has_applied": {
                          "type": "boolean",
                          "description": "Whether current user has applied to this listing"
                        },
                        "application_status": {
                          "type": "string",
                          "nullable": true,
                          "enum": [
                            "pending",
                            "accepted",
                            "rejected",
                            "withdrawn"
                          ],
                          "description": "Current user's application status (if applied)"
                        },
                        "application_id": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Current user's application ID (if applied)"
                        },
                        "is_owner": {
                          "type": "boolean",
                          "description": "Whether current user owns this listing"
                        },
                        "can_edit": {
                          "type": "boolean"
                        },
                        "can_delete": {
                          "type": "boolean"
                        },
                        "applications": {
                          "type": "array",
                          "description": "Owner-only application summaries for this listing",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "status": {
                                "type": "string",
                                "enum": [
                                  "pending",
                                  "accepted",
                                  "rejected",
                                  "withdrawn"
                                ]
                              },
                              "notes": {
                                "type": "string",
                                "nullable": true
                              },
                              "created_at": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "offered_shift": {
                                "type": "object",
                                "nullable": true,
                                "properties": {
                                  "id": {
                                    "type": "integer"
                                  },
                                  "name": {
                                    "type": "string"
                                  },
                                  "start_time": {
                                    "type": "string",
                                    "format": "date-time"
                                  },
                                  "end_time": {
                                    "type": "string",
                                    "format": "date-time"
                                  },
                                  "formatted_date": {
                                    "type": "string"
                                  },
                                  "formatted_time": {
                                    "type": "string"
                                  },
                                  "location": {
                                    "type": "object",
                                    "nullable": true,
                                    "properties": {
                                      "id": {
                                        "type": "integer"
                                      },
                                      "name": {
                                        "type": "string"
                                      }
                                    }
                                  }
                                }
                              },
                              "applicant": {
                                "type": "object",
                                "nullable": true,
                                "properties": {
                                  "id": {
                                    "type": "integer"
                                  },
                                  "name": {
                                    "type": "string"
                                  },
                                  "avatar_url": {
                                    "type": "string",
                                    "nullable": true
                                  },
                                  "avatar_thumbnail_url": {
                                    "type": "string",
                                    "nullable": true
                                  }
                                }
                              }
                            }
                          }
                        },
                        "applications_count": {
                          "type": "integer",
                          "description": "Total applications (owner only)"
                        },
                        "pending_applications_count": {
                          "type": "integer",
                          "description": "Pending applications count (owner only)"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Cannot claim listing (trade-only, conflicts, or requirements not met)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/shift_marketplace/applications": {
      "get": {
        "tags": [
          "Shift Marketplace"
        ],
        "summary": "List User's Applications",
        "description": "Returns applications submitted by the current user for trade-only or both listings",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "Filter by application status",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "accepted",
                "rejected",
                "withdrawn"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of applications",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "An application to a trade listing",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 1
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "pending",
                              "accepted",
                              "rejected",
                              "withdrawn"
                            ],
                            "example": "pending"
                          },
                          "notes": {
                            "type": "string",
                            "nullable": true,
                            "example": "I can work this shift"
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "listing": {
                            "type": "object",
                            "nullable": true,
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "listing_type": {
                                "type": "string"
                              },
                              "status": {
                                "type": "string"
                              },
                              "shift": {
                                "type": "object",
                                "nullable": true,
                                "properties": {
                                  "id": {
                                    "type": "integer"
                                  },
                                  "name": {
                                    "type": "string"
                                  },
                                  "start_time": {
                                    "type": "string",
                                    "format": "date-time"
                                  },
                                  "end_time": {
                                    "type": "string",
                                    "format": "date-time"
                                  },
                                  "formatted_date": {
                                    "type": "string"
                                  },
                                  "formatted_time": {
                                    "type": "string"
                                  },
                                  "location": {
                                    "type": "object",
                                    "nullable": true,
                                    "properties": {
                                      "id": {
                                        "type": "integer"
                                      },
                                      "name": {
                                        "type": "string"
                                      }
                                    }
                                  }
                                }
                              },
                              "created_by": {
                                "type": "object",
                                "nullable": true,
                                "properties": {
                                  "id": {
                                    "type": "integer"
                                  },
                                  "name": {
                                    "type": "string"
                                  },
                                  "email": {
                                    "type": "string"
                                  },
                                  "avatar_url": {
                                    "type": "string",
                                    "nullable": true,
                                    "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                                  },
                                  "avatar_thumbnail_url": {
                                    "type": "string",
                                    "nullable": true,
                                    "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                                  }
                                }
                              }
                            }
                          },
                          "applicant": {
                            "type": "object",
                            "nullable": true,
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string"
                              },
                              "email": {
                                "type": "string"
                              },
                              "avatar_url": {
                                "type": "string",
                                "nullable": true,
                                "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                              },
                              "avatar_thumbnail_url": {
                                "type": "string",
                                "nullable": true,
                                "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                              }
                            }
                          },
                          "is_applicant": {
                            "type": "boolean",
                            "description": "Whether current user is the applicant"
                          },
                          "is_listing_owner": {
                            "type": "boolean",
                            "description": "Whether current user owns the listing"
                          },
                          "can_accept": {
                            "type": "boolean",
                            "description": "Whether current user can accept this application"
                          },
                          "can_reject": {
                            "type": "boolean",
                            "description": "Whether current user can reject this application"
                          },
                          "can_withdraw": {
                            "type": "boolean",
                            "description": "Whether current user can withdraw this application"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 6
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Items per page",
                          "example": 25
                        }
                      },
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required or token invalid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Shift Marketplace"
        ],
        "summary": "Apply to Listing",
        "description": "Submit an application to a trade-only or both listing",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "application"
                ],
                "properties": {
                  "application": {
                    "type": "object",
                    "required": [
                      "listing_id"
                    ],
                    "properties": {
                      "listing_id": {
                        "type": "integer",
                        "example": 123
                      },
                      "notes": {
                        "type": "string",
                        "example": "I can work this shift and have similar experience"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Application submitted successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "application": {
                      "type": "object",
                      "description": "An application to a trade listing",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 1
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "accepted",
                            "rejected",
                            "withdrawn"
                          ],
                          "example": "pending"
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true,
                          "example": "I can work this shift"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "listing": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "listing_type": {
                              "type": "string"
                            },
                            "status": {
                              "type": "string"
                            },
                            "shift": {
                              "type": "object",
                              "nullable": true,
                              "properties": {
                                "id": {
                                  "type": "integer"
                                },
                                "name": {
                                  "type": "string"
                                },
                                "start_time": {
                                  "type": "string",
                                  "format": "date-time"
                                },
                                "end_time": {
                                  "type": "string",
                                  "format": "date-time"
                                },
                                "formatted_date": {
                                  "type": "string"
                                },
                                "formatted_time": {
                                  "type": "string"
                                },
                                "location": {
                                  "type": "object",
                                  "nullable": true,
                                  "properties": {
                                    "id": {
                                      "type": "integer"
                                    },
                                    "name": {
                                      "type": "string"
                                    }
                                  }
                                }
                              }
                            },
                            "created_by": {
                              "type": "object",
                              "nullable": true,
                              "properties": {
                                "id": {
                                  "type": "integer"
                                },
                                "name": {
                                  "type": "string"
                                },
                                "email": {
                                  "type": "string"
                                },
                                "avatar_url": {
                                  "type": "string",
                                  "nullable": true,
                                  "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                                },
                                "avatar_thumbnail_url": {
                                  "type": "string",
                                  "nullable": true,
                                  "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                                }
                              }
                            }
                          }
                        },
                        "applicant": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "email": {
                              "type": "string"
                            },
                            "avatar_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                            },
                            "avatar_thumbnail_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                            }
                          }
                        },
                        "is_applicant": {
                          "type": "boolean",
                          "description": "Whether current user is the applicant"
                        },
                        "is_listing_owner": {
                          "type": "boolean",
                          "description": "Whether current user owns the listing"
                        },
                        "can_accept": {
                          "type": "boolean",
                          "description": "Whether current user can accept this application"
                        },
                        "can_reject": {
                          "type": "boolean",
                          "description": "Whether current user can reject this application"
                        },
                        "can_withdraw": {
                          "type": "boolean",
                          "description": "Whether current user can withdraw this application"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Cannot apply (already applied, listing closed, or wrong type)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/shift_marketplace/applications/{id}": {
      "get": {
        "tags": [
          "Shift Marketplace"
        ],
        "summary": "Get Application Details",
        "description": "View details of a specific application (applicant or listing owner only)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Application details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "application": {
                      "type": "object",
                      "description": "An application to a trade listing",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 1
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "accepted",
                            "rejected",
                            "withdrawn"
                          ],
                          "example": "pending"
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true,
                          "example": "I can work this shift"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "listing": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "listing_type": {
                              "type": "string"
                            },
                            "status": {
                              "type": "string"
                            },
                            "shift": {
                              "type": "object",
                              "nullable": true,
                              "properties": {
                                "id": {
                                  "type": "integer"
                                },
                                "name": {
                                  "type": "string"
                                },
                                "start_time": {
                                  "type": "string",
                                  "format": "date-time"
                                },
                                "end_time": {
                                  "type": "string",
                                  "format": "date-time"
                                },
                                "formatted_date": {
                                  "type": "string"
                                },
                                "formatted_time": {
                                  "type": "string"
                                },
                                "location": {
                                  "type": "object",
                                  "nullable": true,
                                  "properties": {
                                    "id": {
                                      "type": "integer"
                                    },
                                    "name": {
                                      "type": "string"
                                    }
                                  }
                                }
                              }
                            },
                            "created_by": {
                              "type": "object",
                              "nullable": true,
                              "properties": {
                                "id": {
                                  "type": "integer"
                                },
                                "name": {
                                  "type": "string"
                                },
                                "email": {
                                  "type": "string"
                                },
                                "avatar_url": {
                                  "type": "string",
                                  "nullable": true,
                                  "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                                },
                                "avatar_thumbnail_url": {
                                  "type": "string",
                                  "nullable": true,
                                  "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                                }
                              }
                            }
                          }
                        },
                        "applicant": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "email": {
                              "type": "string"
                            },
                            "avatar_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                            },
                            "avatar_thumbnail_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                            }
                          }
                        },
                        "is_applicant": {
                          "type": "boolean",
                          "description": "Whether current user is the applicant"
                        },
                        "is_listing_owner": {
                          "type": "boolean",
                          "description": "Whether current user owns the listing"
                        },
                        "can_accept": {
                          "type": "boolean",
                          "description": "Whether current user can accept this application"
                        },
                        "can_reject": {
                          "type": "boolean",
                          "description": "Whether current user can reject this application"
                        },
                        "can_withdraw": {
                          "type": "boolean",
                          "description": "Whether current user can withdraw this application"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Insufficient permissions to access this resource",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Resource not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/shift_marketplace/applications/{id}/accept": {
      "post": {
        "tags": [
          "Shift Marketplace"
        ],
        "summary": "Accept Application",
        "description": "Accept a pending application (listing owner only).\nThis transfers the shift to the applicant and closes the listing.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Application accepted successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "application": {
                      "type": "object",
                      "description": "An application to a trade listing",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 1
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "accepted",
                            "rejected",
                            "withdrawn"
                          ],
                          "example": "pending"
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true,
                          "example": "I can work this shift"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "listing": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "listing_type": {
                              "type": "string"
                            },
                            "status": {
                              "type": "string"
                            },
                            "shift": {
                              "type": "object",
                              "nullable": true,
                              "properties": {
                                "id": {
                                  "type": "integer"
                                },
                                "name": {
                                  "type": "string"
                                },
                                "start_time": {
                                  "type": "string",
                                  "format": "date-time"
                                },
                                "end_time": {
                                  "type": "string",
                                  "format": "date-time"
                                },
                                "formatted_date": {
                                  "type": "string"
                                },
                                "formatted_time": {
                                  "type": "string"
                                },
                                "location": {
                                  "type": "object",
                                  "nullable": true,
                                  "properties": {
                                    "id": {
                                      "type": "integer"
                                    },
                                    "name": {
                                      "type": "string"
                                    }
                                  }
                                }
                              }
                            },
                            "created_by": {
                              "type": "object",
                              "nullable": true,
                              "properties": {
                                "id": {
                                  "type": "integer"
                                },
                                "name": {
                                  "type": "string"
                                },
                                "email": {
                                  "type": "string"
                                },
                                "avatar_url": {
                                  "type": "string",
                                  "nullable": true,
                                  "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                                },
                                "avatar_thumbnail_url": {
                                  "type": "string",
                                  "nullable": true,
                                  "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                                }
                              }
                            }
                          }
                        },
                        "applicant": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "email": {
                              "type": "string"
                            },
                            "avatar_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                            },
                            "avatar_thumbnail_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                            }
                          }
                        },
                        "is_applicant": {
                          "type": "boolean",
                          "description": "Whether current user is the applicant"
                        },
                        "is_listing_owner": {
                          "type": "boolean",
                          "description": "Whether current user owns the listing"
                        },
                        "can_accept": {
                          "type": "boolean",
                          "description": "Whether current user can accept this application"
                        },
                        "can_reject": {
                          "type": "boolean",
                          "description": "Whether current user can reject this application"
                        },
                        "can_withdraw": {
                          "type": "boolean",
                          "description": "Whether current user can withdraw this application"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Insufficient permissions to access this resource",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Cannot accept (not pending or applicant has conflict)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/shift_marketplace/applications/{id}/reject": {
      "post": {
        "tags": [
          "Shift Marketplace"
        ],
        "summary": "Reject Application",
        "description": "Reject a pending application (listing owner only)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Application rejected",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "application": {
                      "type": "object",
                      "description": "An application to a trade listing",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 1
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "accepted",
                            "rejected",
                            "withdrawn"
                          ],
                          "example": "pending"
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true,
                          "example": "I can work this shift"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "listing": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "listing_type": {
                              "type": "string"
                            },
                            "status": {
                              "type": "string"
                            },
                            "shift": {
                              "type": "object",
                              "nullable": true,
                              "properties": {
                                "id": {
                                  "type": "integer"
                                },
                                "name": {
                                  "type": "string"
                                },
                                "start_time": {
                                  "type": "string",
                                  "format": "date-time"
                                },
                                "end_time": {
                                  "type": "string",
                                  "format": "date-time"
                                },
                                "formatted_date": {
                                  "type": "string"
                                },
                                "formatted_time": {
                                  "type": "string"
                                },
                                "location": {
                                  "type": "object",
                                  "nullable": true,
                                  "properties": {
                                    "id": {
                                      "type": "integer"
                                    },
                                    "name": {
                                      "type": "string"
                                    }
                                  }
                                }
                              }
                            },
                            "created_by": {
                              "type": "object",
                              "nullable": true,
                              "properties": {
                                "id": {
                                  "type": "integer"
                                },
                                "name": {
                                  "type": "string"
                                },
                                "email": {
                                  "type": "string"
                                },
                                "avatar_url": {
                                  "type": "string",
                                  "nullable": true,
                                  "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                                },
                                "avatar_thumbnail_url": {
                                  "type": "string",
                                  "nullable": true,
                                  "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                                }
                              }
                            }
                          }
                        },
                        "applicant": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "email": {
                              "type": "string"
                            },
                            "avatar_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                            },
                            "avatar_thumbnail_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                            }
                          }
                        },
                        "is_applicant": {
                          "type": "boolean",
                          "description": "Whether current user is the applicant"
                        },
                        "is_listing_owner": {
                          "type": "boolean",
                          "description": "Whether current user owns the listing"
                        },
                        "can_accept": {
                          "type": "boolean",
                          "description": "Whether current user can accept this application"
                        },
                        "can_reject": {
                          "type": "boolean",
                          "description": "Whether current user can reject this application"
                        },
                        "can_withdraw": {
                          "type": "boolean",
                          "description": "Whether current user can withdraw this application"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Insufficient permissions to access this resource",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Cannot reject (not pending)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/shift_marketplace/applications/{id}/withdraw": {
      "post": {
        "tags": [
          "Shift Marketplace"
        ],
        "summary": "Withdraw Application",
        "description": "Withdraw a pending application (applicant only)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Application withdrawn",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "application": {
                      "type": "object",
                      "description": "An application to a trade listing",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 1
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "accepted",
                            "rejected",
                            "withdrawn"
                          ],
                          "example": "pending"
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true,
                          "example": "I can work this shift"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "listing": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "listing_type": {
                              "type": "string"
                            },
                            "status": {
                              "type": "string"
                            },
                            "shift": {
                              "type": "object",
                              "nullable": true,
                              "properties": {
                                "id": {
                                  "type": "integer"
                                },
                                "name": {
                                  "type": "string"
                                },
                                "start_time": {
                                  "type": "string",
                                  "format": "date-time"
                                },
                                "end_time": {
                                  "type": "string",
                                  "format": "date-time"
                                },
                                "formatted_date": {
                                  "type": "string"
                                },
                                "formatted_time": {
                                  "type": "string"
                                },
                                "location": {
                                  "type": "object",
                                  "nullable": true,
                                  "properties": {
                                    "id": {
                                      "type": "integer"
                                    },
                                    "name": {
                                      "type": "string"
                                    }
                                  }
                                }
                              }
                            },
                            "created_by": {
                              "type": "object",
                              "nullable": true,
                              "properties": {
                                "id": {
                                  "type": "integer"
                                },
                                "name": {
                                  "type": "string"
                                },
                                "email": {
                                  "type": "string"
                                },
                                "avatar_url": {
                                  "type": "string",
                                  "nullable": true,
                                  "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                                },
                                "avatar_thumbnail_url": {
                                  "type": "string",
                                  "nullable": true,
                                  "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                                }
                              }
                            }
                          }
                        },
                        "applicant": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "email": {
                              "type": "string"
                            },
                            "avatar_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                            },
                            "avatar_thumbnail_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                            }
                          }
                        },
                        "is_applicant": {
                          "type": "boolean",
                          "description": "Whether current user is the applicant"
                        },
                        "is_listing_owner": {
                          "type": "boolean",
                          "description": "Whether current user owns the listing"
                        },
                        "can_accept": {
                          "type": "boolean",
                          "description": "Whether current user can accept this application"
                        },
                        "can_reject": {
                          "type": "boolean",
                          "description": "Whether current user can reject this application"
                        },
                        "can_withdraw": {
                          "type": "boolean",
                          "description": "Whether current user can withdraw this application"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Insufficient permissions to access this resource",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Cannot withdraw (not pending)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/shift_marketplace/direct_offers": {
      "get": {
        "tags": [
          "Shift Marketplace"
        ],
        "summary": "List Direct Offers",
        "description": "Returns direct shift offers (peer-to-peer offers) with filtering options.\n\nDirect offers are one-to-one shift offers where a user offers their shift\nto a specific colleague, unlike marketplace listings which are public.\n\n**Authorization**: Users can only see offers they sent or received.\n**Business Scoping**: Results automatically scoped to current business.\n",
        "parameters": [
          {
            "name": "filter_type",
            "in": "query",
            "description": "Filter offers by type (received/sent)",
            "schema": {
              "type": "string",
              "enum": [
                "received",
                "sent"
              ]
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by offer status",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "accepted",
                "declined",
                "expired",
                "cancelled"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "maximum": 100
            }
          }
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "List of direct offers",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "A direct peer-to-peer shift offer",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 1
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "pending",
                              "accepted",
                              "declined",
                              "expired",
                              "cancelled"
                            ],
                            "example": "pending"
                          },
                          "notes": {
                            "type": "string",
                            "nullable": true
                          },
                          "expires_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "shift": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string"
                              },
                              "start_time": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "end_time": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "formatted_date": {
                                "type": "string"
                              },
                              "formatted_time": {
                                "type": "string"
                              },
                              "location": {
                                "type": "string",
                                "nullable": true
                              }
                            }
                          },
                          "from_user": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string"
                              },
                              "email": {
                                "type": "string"
                              },
                              "avatar_url": {
                                "type": "string",
                                "nullable": true,
                                "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                              },
                              "avatar_thumbnail_url": {
                                "type": "string",
                                "nullable": true,
                                "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                              }
                            }
                          },
                          "to_user": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string"
                              },
                              "email": {
                                "type": "string"
                              },
                              "avatar_url": {
                                "type": "string",
                                "nullable": true,
                                "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                              },
                              "avatar_thumbnail_url": {
                                "type": "string",
                                "nullable": true,
                                "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                              }
                            }
                          },
                          "is_sender": {
                            "type": "boolean",
                            "description": "Whether current user sent this offer"
                          },
                          "is_recipient": {
                            "type": "boolean",
                            "description": "Whether current user received this offer"
                          },
                          "can_accept": {
                            "type": "boolean",
                            "description": "Whether current user can accept this offer"
                          },
                          "can_decline": {
                            "type": "boolean",
                            "description": "Whether current user can decline this offer"
                          },
                          "can_cancel": {
                            "type": "boolean",
                            "description": "Whether current user can cancel this offer (sender only, pending offers)"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 6
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Items per page",
                          "example": 25
                        }
                      },
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required or token invalid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Shift Marketplace"
        ],
        "summary": "Create Direct Offer",
        "description": "Send a direct shift offer to a specific colleague",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "direct_offer"
                ],
                "properties": {
                  "direct_offer": {
                    "type": "object",
                    "required": [
                      "shift_id",
                      "to_user_id"
                    ],
                    "properties": {
                      "shift_id": {
                        "type": "integer",
                        "example": 123
                      },
                      "to_user_id": {
                        "type": "integer",
                        "example": 456
                      },
                      "expires_at": {
                        "type": "string",
                        "format": "date-time",
                        "example": "2025-11-28T10:00:00Z"
                      },
                      "notes": {
                        "type": "string",
                        "example": "Can you cover my shift?"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Direct offer created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "direct_offer": {
                      "type": "object",
                      "description": "A direct peer-to-peer shift offer",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 1
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "accepted",
                            "declined",
                            "expired",
                            "cancelled"
                          ],
                          "example": "pending"
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "shift": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "start_time": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "end_time": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "formatted_date": {
                              "type": "string"
                            },
                            "formatted_time": {
                              "type": "string"
                            },
                            "location": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "from_user": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "email": {
                              "type": "string"
                            },
                            "avatar_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                            },
                            "avatar_thumbnail_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                            }
                          }
                        },
                        "to_user": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "email": {
                              "type": "string"
                            },
                            "avatar_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                            },
                            "avatar_thumbnail_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                            }
                          }
                        },
                        "is_sender": {
                          "type": "boolean",
                          "description": "Whether current user sent this offer"
                        },
                        "is_recipient": {
                          "type": "boolean",
                          "description": "Whether current user received this offer"
                        },
                        "can_accept": {
                          "type": "boolean",
                          "description": "Whether current user can accept this offer"
                        },
                        "can_decline": {
                          "type": "boolean",
                          "description": "Whether current user can decline this offer"
                        },
                        "can_cancel": {
                          "type": "boolean",
                          "description": "Whether current user can cancel this offer (sender only, pending offers)"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          }
        }
      }
    },
    "/shift_marketplace/direct_offers/{id}": {
      "get": {
        "tags": [
          "Shift Marketplace"
        ],
        "summary": "Get Direct Offer Details",
        "description": "View details of a specific direct offer",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Direct offer details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "direct_offer": {
                      "type": "object",
                      "description": "A direct peer-to-peer shift offer",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 1
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "accepted",
                            "declined",
                            "expired",
                            "cancelled"
                          ],
                          "example": "pending"
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "shift": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "start_time": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "end_time": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "formatted_date": {
                              "type": "string"
                            },
                            "formatted_time": {
                              "type": "string"
                            },
                            "location": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "from_user": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "email": {
                              "type": "string"
                            },
                            "avatar_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                            },
                            "avatar_thumbnail_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                            }
                          }
                        },
                        "to_user": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "email": {
                              "type": "string"
                            },
                            "avatar_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                            },
                            "avatar_thumbnail_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                            }
                          }
                        },
                        "is_sender": {
                          "type": "boolean",
                          "description": "Whether current user sent this offer"
                        },
                        "is_recipient": {
                          "type": "boolean",
                          "description": "Whether current user received this offer"
                        },
                        "can_accept": {
                          "type": "boolean",
                          "description": "Whether current user can accept this offer"
                        },
                        "can_decline": {
                          "type": "boolean",
                          "description": "Whether current user can decline this offer"
                        },
                        "can_cancel": {
                          "type": "boolean",
                          "description": "Whether current user can cancel this offer (sender only, pending offers)"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Resource not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/shift_marketplace/direct_offers/{id}/accept": {
      "post": {
        "tags": [
          "Shift Marketplace"
        ],
        "summary": "Accept Direct Offer",
        "description": "Accept a direct offer (recipient only).\nTransfers the shift to the recipient and updates status.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Offer accepted successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "description": "A direct peer-to-peer shift offer",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 1
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "accepted",
                            "declined",
                            "expired",
                            "cancelled"
                          ],
                          "example": "pending"
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "shift": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "start_time": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "end_time": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "formatted_date": {
                              "type": "string"
                            },
                            "formatted_time": {
                              "type": "string"
                            },
                            "location": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "from_user": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "email": {
                              "type": "string"
                            },
                            "avatar_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                            },
                            "avatar_thumbnail_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                            }
                          }
                        },
                        "to_user": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "email": {
                              "type": "string"
                            },
                            "avatar_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                            },
                            "avatar_thumbnail_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                            }
                          }
                        },
                        "is_sender": {
                          "type": "boolean",
                          "description": "Whether current user sent this offer"
                        },
                        "is_recipient": {
                          "type": "boolean",
                          "description": "Whether current user received this offer"
                        },
                        "can_accept": {
                          "type": "boolean",
                          "description": "Whether current user can accept this offer"
                        },
                        "can_decline": {
                          "type": "boolean",
                          "description": "Whether current user can decline this offer"
                        },
                        "can_cancel": {
                          "type": "boolean",
                          "description": "Whether current user can cancel this offer (sender only, pending offers)"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Insufficient permissions to access this resource",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Cannot accept (expired or already processed)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/shift_marketplace/direct_offers/{id}/decline": {
      "post": {
        "tags": [
          "Shift Marketplace"
        ],
        "summary": "Decline Direct Offer",
        "description": "Decline a direct offer (recipient only)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Offer declined successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "description": "A direct peer-to-peer shift offer",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 1
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "accepted",
                            "declined",
                            "expired",
                            "cancelled"
                          ],
                          "example": "pending"
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "shift": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "start_time": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "end_time": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "formatted_date": {
                              "type": "string"
                            },
                            "formatted_time": {
                              "type": "string"
                            },
                            "location": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "from_user": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "email": {
                              "type": "string"
                            },
                            "avatar_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                            },
                            "avatar_thumbnail_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                            }
                          }
                        },
                        "to_user": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "email": {
                              "type": "string"
                            },
                            "avatar_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                            },
                            "avatar_thumbnail_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                            }
                          }
                        },
                        "is_sender": {
                          "type": "boolean",
                          "description": "Whether current user sent this offer"
                        },
                        "is_recipient": {
                          "type": "boolean",
                          "description": "Whether current user received this offer"
                        },
                        "can_accept": {
                          "type": "boolean",
                          "description": "Whether current user can accept this offer"
                        },
                        "can_decline": {
                          "type": "boolean",
                          "description": "Whether current user can decline this offer"
                        },
                        "can_cancel": {
                          "type": "boolean",
                          "description": "Whether current user can cancel this offer (sender only, pending offers)"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Insufficient permissions to access this resource",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Cannot decline (not pending)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/shift_marketplace/direct_offers/{id}/cancel": {
      "post": {
        "tags": [
          "Shift Marketplace"
        ],
        "summary": "Cancel Direct Offer",
        "description": "Cancel a direct offer that you sent (sender only).\nThis withdraws the offer before the recipient responds.\nOnly pending offers can be cancelled.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Offer cancelled successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "description": "A direct peer-to-peer shift offer",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 1
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "accepted",
                            "declined",
                            "expired",
                            "cancelled"
                          ],
                          "example": "pending"
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "shift": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "start_time": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "end_time": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "formatted_date": {
                              "type": "string"
                            },
                            "formatted_time": {
                              "type": "string"
                            },
                            "location": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "from_user": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "email": {
                              "type": "string"
                            },
                            "avatar_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                            },
                            "avatar_thumbnail_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                            }
                          }
                        },
                        "to_user": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "email": {
                              "type": "string"
                            },
                            "avatar_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                            },
                            "avatar_thumbnail_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                            }
                          }
                        },
                        "is_sender": {
                          "type": "boolean",
                          "description": "Whether current user sent this offer"
                        },
                        "is_recipient": {
                          "type": "boolean",
                          "description": "Whether current user received this offer"
                        },
                        "can_accept": {
                          "type": "boolean",
                          "description": "Whether current user can accept this offer"
                        },
                        "can_decline": {
                          "type": "boolean",
                          "description": "Whether current user can decline this offer"
                        },
                        "can_cancel": {
                          "type": "boolean",
                          "description": "Whether current user can cancel this offer (sender only, pending offers)"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Forbidden - can only cancel offers you sent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "422": {
            "description": "Cannot cancel (not pending)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/timesheets": {
      "get": {
        "tags": [
          "Timesheets"
        ],
        "summary": "List user's timesheets",
        "description": "Get a paginated list of timesheets for the authenticated user.\nSupports filtering by status and date ranges.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "description": "Number of items per page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by timesheet status",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "submitted",
                "regional_pending",
                "approved",
                "rejected"
              ],
              "example": "pending"
            }
          },
          {
            "name": "start_date",
            "in": "query",
            "description": "Filter timesheets starting from this date",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2024-01-01"
            }
          },
          {
            "name": "end_date",
            "in": "query",
            "description": "Filter timesheets ending before this date",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2024-12-31"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of timesheets",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "timesheets": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 123
                          },
                          "start_date": {
                            "type": "string",
                            "format": "date",
                            "example": "2024-01-15"
                          },
                          "end_date": {
                            "type": "string",
                            "format": "date",
                            "example": "2024-01-21"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "pending",
                              "submitted",
                              "regional_pending",
                              "approved",
                              "rejected"
                            ],
                            "example": "pending"
                          },
                          "total_hours": {
                            "type": "number",
                            "format": "float",
                            "example": 40.0
                          },
                          "regular_hours": {
                            "type": "number",
                            "format": "float",
                            "example": 40.0
                          },
                          "overtime_hours": {
                            "type": "number",
                            "format": "float",
                            "example": 0.0
                          },
                          "submission_date": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "example": "2024-01-22T09:00:00Z"
                          },
                          "approval_date": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "example": "2024-01-22T14:30:00Z"
                          },
                          "approved_by": {
                            "type": "string",
                            "nullable": true,
                            "example": "John Manager"
                          },
                          "regional_approved": {
                            "type": "boolean",
                            "example": false
                          },
                          "regional_approved_by": {
                            "type": "string",
                            "nullable": true,
                            "example": "Jane Regional"
                          },
                          "regional_approved_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "example": "2024-01-22T16:00:00Z"
                          },
                          "editable": {
                            "type": "boolean",
                            "description": "Whether the timesheet can be edited",
                            "example": true
                          },
                          "submittable": {
                            "type": "boolean",
                            "description": "Whether the timesheet can be submitted",
                            "example": true
                          },
                          "entries_count": {
                            "type": "integer",
                            "description": "Number of entries in this timesheet",
                            "example": 5
                          },
                          "missing_punches_count": {
                            "type": "integer",
                            "description": "Number of entries with missing punches",
                            "example": 0
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2024-01-15T00:00:00Z"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2024-01-21T18:00:00Z"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 6
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Items per page",
                          "example": 25
                        }
                      },
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Timesheets & Payroll feature not enabled",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/timesheets/current": {
      "get": {
        "tags": [
          "Timesheets"
        ],
        "summary": "Get current pay period timesheet",
        "description": "Get the timesheet for the current pay period. If no timesheet exists,\nit will be automatically generated based on attendance records and shifts.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Current timesheet",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "timesheet": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Timesheet"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "entries": {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/TimesheetEntry"
                              }
                            },
                            "entries_by_date": {
                              "type": "object",
                              "description": "Entries grouped by date",
                              "additionalProperties": {
                                "type": "array",
                                "items": {
                                  "$ref": "#/components/schemas/TimesheetEntry"
                                }
                              }
                            },
                            "summary": {
                              "$ref": "#/components/schemas/TimesheetSummary"
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "No pay period configuration or timesheet generation failed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/timesheets/{id}": {
      "get": {
        "tags": [
          "Timesheets"
        ],
        "summary": "Get timesheet details",
        "description": "Get detailed information about a specific timesheet including all entries,\nsummary statistics, and approval status.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Timesheet ID",
            "schema": {
              "type": "integer",
              "example": 123
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Timesheet details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "timesheet": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Timesheet"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "entries": {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/TimesheetEntry"
                              }
                            },
                            "entries_by_date": {
                              "type": "object",
                              "description": "Entries grouped by date",
                              "additionalProperties": {
                                "type": "array",
                                "items": {
                                  "$ref": "#/components/schemas/TimesheetEntry"
                                }
                              }
                            },
                            "summary": {
                              "$ref": "#/components/schemas/TimesheetSummary"
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Timesheet not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Timesheets"
        ],
        "summary": "Update timesheet entries",
        "description": "Update timesheet entries with new times or notes. Only editable timesheets\n(pending or rejected status) can be updated.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Timesheet ID",
            "schema": {
              "type": "integer",
              "example": 123
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "timesheet_entries": {
                    "type": "object",
                    "description": "Map of entry IDs to update parameters",
                    "additionalProperties": {
                      "type": "object",
                      "required": [
                        "edit_reason"
                      ],
                      "properties": {
                        "start_time": {
                          "type": "string",
                          "format": "time",
                          "example": "09:00:00"
                        },
                        "end_time": {
                          "type": "string",
                          "format": "time",
                          "example": "17:00:00"
                        },
                        "edit_reason": {
                          "type": "string",
                          "description": "Required whenever a punch actually changes — the reason is stored on the entry and in its edit history. An entry whose times do not move is a no-op and needs none; a changed entry sent without one is refused (422 edit_reason_required) and no entry in the request is written.",
                          "example": "Corrected clock-in time"
                        }
                      }
                    }
                  },
                  "timesheet": {
                    "type": "object",
                    "properties": {
                      "notes": {
                        "type": "string",
                        "description": "The employee's own note on the timesheet. This endpoint is scoped to the caller's own timesheets, so manager_notes — the approver's field, shown back to the employee as the rejection reason — is deliberately not writable here.",
                        "example": "Updated per employee request"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Timesheet updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "timesheet": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Timesheet"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "entries": {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/TimesheetEntry"
                              }
                            },
                            "entries_by_date": {
                              "type": "object",
                              "description": "Entries grouped by date",
                              "additionalProperties": {
                                "type": "array",
                                "items": {
                                  "$ref": "#/components/schemas/TimesheetEntry"
                                }
                              }
                            },
                            "summary": {
                              "$ref": "#/components/schemas/TimesheetSummary"
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Update failed, timesheet not editable, or a changed entry was sent without an edit_reason (edit_reason_required — the error details name every offending entry id, and nothing in the request is written).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/timesheets/{id}/submit": {
      "post": {
        "tags": [
          "Timesheets"
        ],
        "summary": "Submit timesheet for approval",
        "description": "Submit a timesheet for manager approval. The timesheet must be in pending\nor rejected status and have at least one entry.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Timesheet ID",
            "schema": {
              "type": "integer",
              "example": 123
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Timesheet submitted successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "timesheet": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Timesheet"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "entries": {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/TimesheetEntry"
                              }
                            },
                            "entries_by_date": {
                              "type": "object",
                              "description": "Entries grouped by date",
                              "additionalProperties": {
                                "type": "array",
                                "items": {
                                  "$ref": "#/components/schemas/TimesheetEntry"
                                }
                              }
                            },
                            "summary": {
                              "$ref": "#/components/schemas/TimesheetSummary"
                            }
                          }
                        }
                      ]
                    },
                    "submission": {
                      "type": "object",
                      "properties": {
                        "resubmitted": {
                          "type": "boolean",
                          "description": "Whether this was a resubmission after rejection"
                        },
                        "accuracy_score": {
                          "type": "number",
                          "format": "float",
                          "description": "Calculated accuracy score (if available)",
                          "example": 95.5
                        },
                        "score_error": {
                          "type": "string",
                          "description": "Error message if score calculation failed"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Timesheet cannot be submitted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/timesheets/{id}/entries": {
      "get": {
        "tags": [
          "Timesheets"
        ],
        "summary": "Get timesheet entries",
        "description": "Get all entries for a specific timesheet with detailed information\nincluding associated shifts and attendance records.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Timesheet ID",
            "schema": {
              "type": "integer",
              "example": 123
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Timesheet entries",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "entries": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 456
                          },
                          "date": {
                            "type": "string",
                            "format": "date",
                            "example": "2024-01-15"
                          },
                          "start_time": {
                            "type": "string",
                            "format": "time",
                            "nullable": true,
                            "example": "09:00:00"
                          },
                          "end_time": {
                            "type": "string",
                            "format": "time",
                            "nullable": true,
                            "example": "17:00:00"
                          },
                          "hours": {
                            "type": "number",
                            "format": "float",
                            "nullable": true,
                            "example": 8.0
                          },
                          "edited": {
                            "type": "boolean",
                            "description": "Whether this entry has been manually edited",
                            "example": false
                          },
                          "edit_notes": {
                            "type": "string",
                            "nullable": true,
                            "description": "Notes about edits made to this entry",
                            "example": "Corrected clock-in time"
                          },
                          "missing_punch": {
                            "type": "boolean",
                            "description": "Whether this entry is missing clock-in or clock-out",
                            "example": false
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "pending",
                              "approved",
                              "rejected"
                            ],
                            "example": "pending"
                          },
                          "attendance_record_id": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Associated attendance record ID",
                            "example": 789
                          },
                          "shift_id": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Associated shift ID",
                            "example": 101
                          },
                          "shift": {
                            "type": "object",
                            "nullable": true,
                            "description": "Associated shift information",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 101
                              },
                              "title": {
                                "type": "string",
                                "example": "Morning Shift"
                              },
                              "location": {
                                "type": "string",
                                "nullable": true,
                                "example": "Main Office"
                              },
                              "is_ad_hoc": {
                                "type": "boolean",
                                "example": false
                              }
                            }
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2024-01-15T09:00:00Z"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2024-01-15T17:00:00Z"
                          }
                        }
                      }
                    },
                    "timesheet_summary": {
                      "type": "object",
                      "properties": {
                        "total_hours": {
                          "type": "number",
                          "format": "float",
                          "example": 40.0
                        },
                        "regular_hours": {
                          "type": "number",
                          "format": "float",
                          "example": 40.0
                        },
                        "overtime_hours": {
                          "type": "number",
                          "format": "float",
                          "example": 0.0
                        },
                        "total_entries": {
                          "type": "integer",
                          "example": 5
                        },
                        "missing_punches": {
                          "type": "integer",
                          "example": 0
                        },
                        "edited_entries": {
                          "type": "integer",
                          "example": 1
                        },
                        "days_with_entries": {
                          "type": "integer",
                          "example": 5
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Timesheet not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/timesheet_entries": {
      "get": {
        "tags": [
          "Timesheet Entries"
        ],
        "summary": "List timesheet entries",
        "description": "Get a paginated list of timesheet entries across all user's timesheets\nor for a specific timesheet. Supports various filters.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "description": "Number of items per page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "timesheet_id",
            "in": "query",
            "description": "Filter entries for specific timesheet",
            "schema": {
              "type": "integer",
              "example": 123
            }
          },
          {
            "name": "start_date",
            "in": "query",
            "description": "Filter entries from this date",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2024-01-01"
            }
          },
          {
            "name": "end_date",
            "in": "query",
            "description": "Filter entries to this date",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2024-01-31"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by entry status",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "approved",
                "rejected"
              ],
              "example": "pending"
            }
          },
          {
            "name": "edited",
            "in": "query",
            "description": "Filter by edited status",
            "schema": {
              "type": "boolean",
              "example": true
            }
          },
          {
            "name": "missing_punch",
            "in": "query",
            "description": "Filter by missing punch status",
            "schema": {
              "type": "boolean",
              "example": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of timesheet entries",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "entries": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 456
                          },
                          "date": {
                            "type": "string",
                            "format": "date",
                            "example": "2024-01-15"
                          },
                          "start_time": {
                            "type": "string",
                            "format": "time",
                            "nullable": true,
                            "example": "09:00:00"
                          },
                          "end_time": {
                            "type": "string",
                            "format": "time",
                            "nullable": true,
                            "example": "17:00:00"
                          },
                          "hours": {
                            "type": "number",
                            "format": "float",
                            "nullable": true,
                            "example": 8.0
                          },
                          "edited": {
                            "type": "boolean",
                            "description": "Whether this entry has been manually edited",
                            "example": false
                          },
                          "edit_notes": {
                            "type": "string",
                            "nullable": true,
                            "description": "Notes about edits made to this entry",
                            "example": "Corrected clock-in time"
                          },
                          "missing_punch": {
                            "type": "boolean",
                            "description": "Whether this entry is missing clock-in or clock-out",
                            "example": false
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "pending",
                              "approved",
                              "rejected"
                            ],
                            "example": "pending"
                          },
                          "attendance_record_id": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Associated attendance record ID",
                            "example": 789
                          },
                          "shift_id": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Associated shift ID",
                            "example": 101
                          },
                          "shift": {
                            "type": "object",
                            "nullable": true,
                            "description": "Associated shift information",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 101
                              },
                              "title": {
                                "type": "string",
                                "example": "Morning Shift"
                              },
                              "location": {
                                "type": "string",
                                "nullable": true,
                                "example": "Main Office"
                              },
                              "is_ad_hoc": {
                                "type": "boolean",
                                "example": false
                              }
                            }
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2024-01-15T09:00:00Z"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2024-01-15T17:00:00Z"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 6
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Items per page",
                          "example": 25
                        }
                      },
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Timesheet Entries"
        ],
        "summary": "Create manual timesheet entry",
        "description": "Create a manual timesheet entry for times not captured through\nattendance records. The timesheet must be editable.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "timesheet_entry"
                ],
                "properties": {
                  "timesheet_entry": {
                    "type": "object",
                    "required": [
                      "timesheet_id",
                      "date",
                      "start_time",
                      "end_time"
                    ],
                    "properties": {
                      "timesheet_id": {
                        "type": "integer",
                        "example": 123
                      },
                      "date": {
                        "type": "string",
                        "format": "date",
                        "example": "2024-01-15"
                      },
                      "start_time": {
                        "type": "string",
                        "format": "time",
                        "example": "09:00:00"
                      },
                      "end_time": {
                        "type": "string",
                        "format": "time",
                        "example": "17:00:00"
                      },
                      "hours": {
                        "type": "number",
                        "format": "float",
                        "description": "Hours worked (calculated if not provided)",
                        "example": 8.0
                      },
                      "edit_notes": {
                        "type": "string",
                        "example": "Manual entry for missed punch"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Timesheet entry created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "entry": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/TimesheetEntry"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "timesheet": {
                              "type": "object",
                              "description": "Associated timesheet information",
                              "properties": {
                                "id": {
                                  "type": "integer",
                                  "example": 123
                                },
                                "start_date": {
                                  "type": "string",
                                  "format": "date",
                                  "example": "2024-01-15"
                                },
                                "end_date": {
                                  "type": "string",
                                  "format": "date",
                                  "example": "2024-01-21"
                                },
                                "status": {
                                  "type": "string",
                                  "example": "pending"
                                }
                              }
                            },
                            "edit_history": {
                              "type": "array",
                              "description": "History of edits made to this entry",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 1
                                  },
                                  "field_changed": {
                                    "type": "string",
                                    "example": "start_time"
                                  },
                                  "original_value": {
                                    "type": "string",
                                    "example": "08:45:00"
                                  },
                                  "new_value": {
                                    "type": "string",
                                    "example": "09:00:00"
                                  },
                                  "reason": {
                                    "type": "string",
                                    "example": "Employee correction"
                                  },
                                  "edited_by": {
                                    "type": "string",
                                    "example": "John Employee"
                                  },
                                  "edited_at": {
                                    "type": "string",
                                    "format": "date-time",
                                    "example": "2024-01-15T10:00:00Z"
                                  }
                                }
                              }
                            },
                            "attendance_record": {
                              "type": "object",
                              "nullable": true,
                              "description": "Associated attendance record details",
                              "properties": {
                                "id": {
                                  "type": "integer",
                                  "example": 789
                                },
                                "check_in_time": {
                                  "type": "string",
                                  "format": "date-time",
                                  "nullable": true,
                                  "example": "2024-01-15T09:00:00Z"
                                },
                                "check_out_time": {
                                  "type": "string",
                                  "format": "date-time",
                                  "nullable": true,
                                  "example": "2024-01-15T17:00:00Z"
                                },
                                "status": {
                                  "type": "string",
                                  "example": "completed"
                                },
                                "requires_review": {
                                  "type": "boolean",
                                  "example": false
                                }
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Validation failed or timesheet not editable",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/timesheet_entries/{id}": {
      "get": {
        "tags": [
          "Timesheet Entries"
        ],
        "summary": "Get timesheet entry details",
        "description": "Get detailed information about a specific timesheet entry including\nedit history and associated records.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Timesheet entry ID",
            "schema": {
              "type": "integer",
              "example": 456
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Timesheet entry details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "entry": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/TimesheetEntry"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "timesheet": {
                              "type": "object",
                              "description": "Associated timesheet information",
                              "properties": {
                                "id": {
                                  "type": "integer",
                                  "example": 123
                                },
                                "start_date": {
                                  "type": "string",
                                  "format": "date",
                                  "example": "2024-01-15"
                                },
                                "end_date": {
                                  "type": "string",
                                  "format": "date",
                                  "example": "2024-01-21"
                                },
                                "status": {
                                  "type": "string",
                                  "example": "pending"
                                }
                              }
                            },
                            "edit_history": {
                              "type": "array",
                              "description": "History of edits made to this entry",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 1
                                  },
                                  "field_changed": {
                                    "type": "string",
                                    "example": "start_time"
                                  },
                                  "original_value": {
                                    "type": "string",
                                    "example": "08:45:00"
                                  },
                                  "new_value": {
                                    "type": "string",
                                    "example": "09:00:00"
                                  },
                                  "reason": {
                                    "type": "string",
                                    "example": "Employee correction"
                                  },
                                  "edited_by": {
                                    "type": "string",
                                    "example": "John Employee"
                                  },
                                  "edited_at": {
                                    "type": "string",
                                    "format": "date-time",
                                    "example": "2024-01-15T10:00:00Z"
                                  }
                                }
                              }
                            },
                            "attendance_record": {
                              "type": "object",
                              "nullable": true,
                              "description": "Associated attendance record details",
                              "properties": {
                                "id": {
                                  "type": "integer",
                                  "example": 789
                                },
                                "check_in_time": {
                                  "type": "string",
                                  "format": "date-time",
                                  "nullable": true,
                                  "example": "2024-01-15T09:00:00Z"
                                },
                                "check_out_time": {
                                  "type": "string",
                                  "format": "date-time",
                                  "nullable": true,
                                  "example": "2024-01-15T17:00:00Z"
                                },
                                "status": {
                                  "type": "string",
                                  "example": "completed"
                                },
                                "requires_review": {
                                  "type": "boolean",
                                  "example": false
                                }
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Timesheet entry not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Timesheet Entries"
        ],
        "summary": "Update timesheet entry",
        "description": "Update a timesheet entry with new times or notes. The associated\ntimesheet must be editable.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Timesheet entry ID",
            "schema": {
              "type": "integer",
              "example": 456
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "timesheet_entry"
                ],
                "properties": {
                  "timesheet_entry": {
                    "type": "object",
                    "properties": {
                      "date": {
                        "type": "string",
                        "format": "date",
                        "example": "2024-01-15"
                      },
                      "start_time": {
                        "type": "string",
                        "format": "time",
                        "example": "09:00:00"
                      },
                      "end_time": {
                        "type": "string",
                        "format": "time",
                        "example": "17:00:00"
                      },
                      "hours": {
                        "type": "number",
                        "format": "float",
                        "example": 8.0
                      },
                      "edit_notes": {
                        "type": "string",
                        "example": "Corrected end time"
                      }
                    }
                  },
                  "edit_reason": {
                    "type": "string",
                    "description": "Reason for the edit (for audit trail)",
                    "example": "Employee requested time correction"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Timesheet entry updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "entry": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/TimesheetEntry"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "timesheet": {
                              "type": "object",
                              "description": "Associated timesheet information",
                              "properties": {
                                "id": {
                                  "type": "integer",
                                  "example": 123
                                },
                                "start_date": {
                                  "type": "string",
                                  "format": "date",
                                  "example": "2024-01-15"
                                },
                                "end_date": {
                                  "type": "string",
                                  "format": "date",
                                  "example": "2024-01-21"
                                },
                                "status": {
                                  "type": "string",
                                  "example": "pending"
                                }
                              }
                            },
                            "edit_history": {
                              "type": "array",
                              "description": "History of edits made to this entry",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 1
                                  },
                                  "field_changed": {
                                    "type": "string",
                                    "example": "start_time"
                                  },
                                  "original_value": {
                                    "type": "string",
                                    "example": "08:45:00"
                                  },
                                  "new_value": {
                                    "type": "string",
                                    "example": "09:00:00"
                                  },
                                  "reason": {
                                    "type": "string",
                                    "example": "Employee correction"
                                  },
                                  "edited_by": {
                                    "type": "string",
                                    "example": "John Employee"
                                  },
                                  "edited_at": {
                                    "type": "string",
                                    "format": "date-time",
                                    "example": "2024-01-15T10:00:00Z"
                                  }
                                }
                              }
                            },
                            "attendance_record": {
                              "type": "object",
                              "nullable": true,
                              "description": "Associated attendance record details",
                              "properties": {
                                "id": {
                                  "type": "integer",
                                  "example": 789
                                },
                                "check_in_time": {
                                  "type": "string",
                                  "format": "date-time",
                                  "nullable": true,
                                  "example": "2024-01-15T09:00:00Z"
                                },
                                "check_out_time": {
                                  "type": "string",
                                  "format": "date-time",
                                  "nullable": true,
                                  "example": "2024-01-15T17:00:00Z"
                                },
                                "status": {
                                  "type": "string",
                                  "example": "completed"
                                },
                                "requires_review": {
                                  "type": "boolean",
                                  "example": false
                                }
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Update failed or timesheet not editable",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Timesheet Entries"
        ],
        "summary": "Delete timesheet entry",
        "description": "Delete a manual timesheet entry. Only entries not tied to attendance\nrecords can be deleted, and the timesheet must be editable.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Timesheet entry ID",
            "schema": {
              "type": "integer",
              "example": 456
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Timesheet entry deleted successfully"
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Cannot delete entry (tied to attendance or timesheet not editable)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/pay_information/summary": {
      "get": {
        "tags": [
          "Pay Information"
        ],
        "summary": "Get current pay period summary",
        "description": "Get a summary of the current pay period including hours worked,\nestimated pay, and timesheet status.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Pay period summary",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "pay_summary": {
                      "type": "object",
                      "properties": {
                        "pay_period": {
                          "type": "object",
                          "properties": {
                            "start_date": {
                              "type": "string",
                              "format": "date",
                              "example": "2024-01-15"
                            },
                            "end_date": {
                              "type": "string",
                              "format": "date",
                              "example": "2024-01-21"
                            },
                            "period_type": {
                              "type": "string",
                              "enum": [
                                "weekly",
                                "bi-weekly",
                                "semi-monthly",
                                "monthly"
                              ],
                              "example": "weekly"
                            }
                          }
                        },
                        "hours": {
                          "type": "object",
                          "properties": {
                            "regular_hours": {
                              "type": "number",
                              "format": "float",
                              "example": 40.0
                            },
                            "overtime_hours": {
                              "type": "number",
                              "format": "float",
                              "example": 2.5
                            },
                            "total_hours": {
                              "type": "number",
                              "format": "float",
                              "example": 42.5
                            }
                          }
                        },
                        "estimated_pay": {
                          "type": "object",
                          "properties": {
                            "regular_pay": {
                              "type": "number",
                              "format": "float",
                              "example": 1000.0
                            },
                            "overtime_pay": {
                              "type": "number",
                              "format": "float",
                              "example": 93.75
                            },
                            "total_pay": {
                              "type": "number",
                              "format": "float",
                              "example": 1093.75
                            },
                            "hourly_rate": {
                              "type": "number",
                              "format": "float",
                              "example": 25.0
                            },
                            "overtime_rate": {
                              "type": "number",
                              "format": "float",
                              "example": 37.5
                            }
                          }
                        },
                        "timesheet_status": {
                          "type": "string",
                          "enum": [
                            "not_created",
                            "pending",
                            "submitted",
                            "approved",
                            "rejected"
                          ],
                          "example": "pending"
                        },
                        "last_updated": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2024-01-20T10:30:00Z"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "No pay period configuration found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/pay_information/history": {
      "get": {
        "tags": [
          "Pay Information"
        ],
        "summary": "Get pay history",
        "description": "Get historical pay information from approved timesheets and paychecks.\nIncludes both estimated pay from timesheets and actual pay from paychecks.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "description": "Number of items per page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Pay history",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "pay_history": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "type": {
                            "type": "string",
                            "enum": [
                              "timesheet",
                              "paycheck"
                            ],
                            "example": "timesheet"
                          },
                          "id": {
                            "type": "integer",
                            "example": 123
                          },
                          "period_start": {
                            "type": "string",
                            "format": "date",
                            "example": "2024-01-15"
                          },
                          "period_end": {
                            "type": "string",
                            "format": "date",
                            "example": "2024-01-21"
                          },
                          "regular_hours": {
                            "type": "number",
                            "format": "float",
                            "example": 40.0
                          },
                          "overtime_hours": {
                            "type": "number",
                            "format": "float",
                            "example": 2.5
                          },
                          "total_hours": {
                            "type": "number",
                            "format": "float",
                            "example": 42.5
                          },
                          "status": {
                            "type": "string",
                            "description": "Status (for timesheets)",
                            "example": "approved"
                          },
                          "approved_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "Approval date (for timesheets)",
                            "example": "2024-01-22T14:30:00Z"
                          },
                          "estimated_pay": {
                            "type": "number",
                            "format": "float",
                            "description": "Estimated pay (for timesheets)",
                            "example": 1093.75
                          },
                          "gross_pay": {
                            "type": "number",
                            "format": "float",
                            "description": "Gross pay (for paychecks)",
                            "example": 1093.75
                          },
                          "net_pay": {
                            "type": "number",
                            "format": "float",
                            "description": "Net pay (for paychecks)",
                            "example": 850.0
                          },
                          "pay_date": {
                            "type": "string",
                            "format": "date",
                            "description": "Pay date (for paychecks)",
                            "example": "2024-01-26"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 6
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Items per page",
                          "example": 25
                        }
                      },
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/pay_information/current_period": {
      "get": {
        "tags": [
          "Pay Information"
        ],
        "summary": "Get current pay period details",
        "description": "Get detailed information about the current pay period including\ntimesheet status, estimated pay, and next payday.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Current pay period details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "current_period": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "example": "2024-01-15_2024-01-21"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-01-15"
                        },
                        "end_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-01-21"
                        },
                        "period_type": {
                          "type": "string",
                          "enum": [
                            "weekly",
                            "bi-weekly",
                            "semi-monthly",
                            "monthly"
                          ],
                          "example": "weekly"
                        },
                        "days_in_period": {
                          "type": "integer",
                          "example": 7
                        },
                        "is_current": {
                          "type": "boolean",
                          "example": true
                        },
                        "is_future": {
                          "type": "boolean",
                          "example": false
                        },
                        "is_past": {
                          "type": "boolean",
                          "example": false
                        },
                        "payday": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-01-26"
                        },
                        "days_until_payday": {
                          "type": "integer",
                          "description": "Days until payday (for current period)",
                          "example": 5
                        },
                        "days_remaining": {
                          "type": "integer",
                          "description": "Days remaining in period (for current period)",
                          "example": 2
                        },
                        "timesheet": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 123
                            },
                            "status": {
                              "type": "string",
                              "enum": [
                                "pending",
                                "submitted",
                                "approved",
                                "rejected"
                              ],
                              "example": "pending"
                            },
                            "total_hours": {
                              "type": "number",
                              "format": "float",
                              "example": 42.5
                            },
                            "regular_hours": {
                              "type": "number",
                              "format": "float",
                              "example": 40.0
                            },
                            "overtime_hours": {
                              "type": "number",
                              "format": "float",
                              "example": 2.5
                            },
                            "submission_date": {
                              "type": "string",
                              "format": "date-time",
                              "example": "2024-01-21T17:00:00Z"
                            },
                            "approval_date": {
                              "type": "string",
                              "format": "date-time",
                              "example": "2024-01-22T09:00:00Z"
                            },
                            "editable": {
                              "type": "boolean",
                              "example": true
                            },
                            "submittable": {
                              "type": "boolean",
                              "example": true
                            }
                          }
                        },
                        "estimated_pay": {
                          "type": "number",
                          "format": "float",
                          "example": 1093.75
                        },
                        "work_days": {
                          "type": "integer",
                          "example": 5
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "No pay period configuration found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/pay_information/next_payday": {
      "get": {
        "tags": [
          "Pay Information"
        ],
        "summary": "Get next payday information",
        "description": "Calculate and return the next payday date based on current\npay period and business pay schedule.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Next payday information",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "next_payday": {
                      "type": "object",
                      "properties": {
                        "date": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-01-26"
                        },
                        "days_until": {
                          "type": "integer",
                          "example": 5
                        },
                        "period_start": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-01-15"
                        },
                        "period_end": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-01-21"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "No pay period configuration found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/pay_information/ytd_summary": {
      "get": {
        "tags": [
          "Pay Information"
        ],
        "summary": "Get year-to-date summary",
        "description": "Get year-to-date summary including total hours worked,\nestimated and actual pay, and timesheet statistics.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Year-to-date summary",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ytd_summary": {
                      "type": "object",
                      "properties": {
                        "year": {
                          "type": "integer",
                          "example": 2024
                        },
                        "period": {
                          "type": "object",
                          "properties": {
                            "start_date": {
                              "type": "string",
                              "format": "date",
                              "example": "2024-01-01"
                            },
                            "end_date": {
                              "type": "string",
                              "format": "date",
                              "example": "2024-12-31"
                            },
                            "days_elapsed": {
                              "type": "integer",
                              "example": 20
                            },
                            "days_remaining": {
                              "type": "integer",
                              "example": 345
                            }
                          }
                        },
                        "hours": {
                          "type": "object",
                          "properties": {
                            "regular_hours": {
                              "type": "number",
                              "format": "float",
                              "example": 160.0
                            },
                            "overtime_hours": {
                              "type": "number",
                              "format": "float",
                              "example": 10.0
                            },
                            "total_hours": {
                              "type": "number",
                              "format": "float",
                              "example": 170.0
                            },
                            "average_weekly_hours": {
                              "type": "number",
                              "format": "float",
                              "example": 42.5
                            }
                          }
                        },
                        "estimated_pay": {
                          "type": "object",
                          "properties": {
                            "regular_pay": {
                              "type": "number",
                              "format": "float",
                              "example": 4000.0
                            },
                            "overtime_pay": {
                              "type": "number",
                              "format": "float",
                              "example": 375.0
                            },
                            "total_pay": {
                              "type": "number",
                              "format": "float",
                              "example": 4375.0
                            }
                          }
                        },
                        "actual_pay": {
                          "type": "object",
                          "properties": {
                            "gross_pay": {
                              "type": "number",
                              "format": "float",
                              "example": 4200.0
                            },
                            "net_pay": {
                              "type": "number",
                              "format": "float",
                              "example": 3200.0
                            },
                            "paychecks_count": {
                              "type": "integer",
                              "example": 4
                            }
                          }
                        },
                        "timesheets": {
                          "type": "object",
                          "properties": {
                            "total_count": {
                              "type": "integer",
                              "example": 4
                            },
                            "approved_count": {
                              "type": "integer",
                              "example": 3
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/paychecks": {
      "get": {
        "tags": [
          "Paychecks"
        ],
        "summary": "List user's paychecks",
        "description": "Get a paginated list of paychecks for the authenticated user.\nRequires Payroll Connect to be enabled and configured.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "description": "Number of items per page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "start_date",
            "in": "query",
            "description": "Filter paychecks from this date",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2024-01-01"
            }
          },
          {
            "name": "end_date",
            "in": "query",
            "description": "Filter paychecks to this date",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2024-12-31"
            }
          },
          {
            "name": "year",
            "in": "query",
            "description": "Filter paychecks by year",
            "schema": {
              "type": "integer",
              "example": 2024
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of paychecks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "paychecks": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 123
                          },
                          "pay_date": {
                            "type": "string",
                            "format": "date",
                            "example": "2024-01-26"
                          },
                          "pay_period_start": {
                            "type": "string",
                            "format": "date",
                            "example": "2024-01-15"
                          },
                          "pay_period_end": {
                            "type": "string",
                            "format": "date",
                            "example": "2024-01-21"
                          },
                          "regular_hours": {
                            "type": "number",
                            "format": "float",
                            "example": 40.0
                          },
                          "overtime_hours": {
                            "type": "number",
                            "format": "float",
                            "example": 2.5
                          },
                          "total_hours": {
                            "type": "number",
                            "format": "float",
                            "example": 42.5
                          },
                          "gross_pay": {
                            "type": "number",
                            "format": "float",
                            "example": 1093.75
                          },
                          "net_pay": {
                            "type": "number",
                            "format": "float",
                            "example": 850.0
                          },
                          "pay_frequency": {
                            "type": "string",
                            "enum": [
                              "weekly",
                              "bi-weekly",
                              "semi-monthly",
                              "monthly"
                            ],
                            "example": "weekly"
                          },
                          "has_file": {
                            "type": "boolean",
                            "example": true
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2024-01-26T08:00:00Z"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2024-01-26T08:00:00Z"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 6
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Items per page",
                          "example": 25
                        }
                      },
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Payroll Connect not enabled",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/paychecks/recent": {
      "get": {
        "tags": [
          "Paychecks"
        ],
        "summary": "Get recent paychecks",
        "description": "Get the most recent paychecks (last 6 months, up to 12 paychecks).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Recent paychecks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "recent_paychecks": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 123
                          },
                          "pay_date": {
                            "type": "string",
                            "format": "date",
                            "example": "2024-01-26"
                          },
                          "pay_period_start": {
                            "type": "string",
                            "format": "date",
                            "example": "2024-01-15"
                          },
                          "pay_period_end": {
                            "type": "string",
                            "format": "date",
                            "example": "2024-01-21"
                          },
                          "regular_hours": {
                            "type": "number",
                            "format": "float",
                            "example": 40.0
                          },
                          "overtime_hours": {
                            "type": "number",
                            "format": "float",
                            "example": 2.5
                          },
                          "total_hours": {
                            "type": "number",
                            "format": "float",
                            "example": 42.5
                          },
                          "gross_pay": {
                            "type": "number",
                            "format": "float",
                            "example": 1093.75
                          },
                          "net_pay": {
                            "type": "number",
                            "format": "float",
                            "example": 850.0
                          },
                          "pay_frequency": {
                            "type": "string",
                            "enum": [
                              "weekly",
                              "bi-weekly",
                              "semi-monthly",
                              "monthly"
                            ],
                            "example": "weekly"
                          },
                          "has_file": {
                            "type": "boolean",
                            "example": true
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2024-01-26T08:00:00Z"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2024-01-26T08:00:00Z"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Payroll Connect not enabled",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/paychecks/{id}": {
      "get": {
        "tags": [
          "Paychecks"
        ],
        "summary": "Get paycheck details",
        "description": "Get detailed paycheck information including earnings, deductions,\ntaxes, and year-to-date totals.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Paycheck ID",
            "schema": {
              "type": "integer",
              "example": 123
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paycheck details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "paycheck": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Paycheck"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "earnings": {
                              "type": "object",
                              "properties": {
                                "regular_pay": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 1000.0
                                },
                                "overtime_pay": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 93.75
                                },
                                "holiday_pay": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 0.0
                                },
                                "sick_pay": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 0.0
                                },
                                "vacation_pay": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 0.0
                                },
                                "bonus": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 0.0
                                },
                                "commission": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 0.0
                                },
                                "other_earnings": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 0.0
                                },
                                "gross_pay": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 1093.75
                                }
                              }
                            },
                            "deductions": {
                              "type": "object",
                              "properties": {
                                "health_insurance": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 125.0
                                },
                                "dental_insurance": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 15.0
                                },
                                "vision_insurance": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 5.0
                                },
                                "life_insurance": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 10.0
                                },
                                "retirement_401k": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 50.0
                                },
                                "retirement_roth": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 0.0
                                },
                                "hsa": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 25.0
                                },
                                "fsa": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 0.0
                                },
                                "parking": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 20.0
                                },
                                "union_dues": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 0.0
                                },
                                "other_deductions": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 0.0
                                },
                                "total_deductions": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 250.0
                                }
                              }
                            },
                            "taxes": {
                              "type": "object",
                              "properties": {
                                "federal_income_tax": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 150.0
                                },
                                "state_income_tax": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 50.0
                                },
                                "local_income_tax": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 10.0
                                },
                                "social_security": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 67.81
                                },
                                "medicare": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 15.86
                                },
                                "unemployment_tax": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 0.0
                                },
                                "disability_tax": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 5.0
                                },
                                "other_taxes": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 0.0
                                },
                                "total_taxes": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 298.67
                                }
                              }
                            },
                            "employer_contributions": {
                              "type": "object",
                              "properties": {
                                "health_insurance": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 200.0
                                },
                                "retirement_match": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 25.0
                                },
                                "social_security": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 67.81
                                },
                                "medicare": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 15.86
                                },
                                "unemployment": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 6.56
                                },
                                "workers_comp": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 10.94
                                },
                                "other_contributions": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 0.0
                                },
                                "total_contributions": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 326.17
                                }
                              }
                            },
                            "year_to_date": {
                              "type": "object",
                              "properties": {
                                "gross_pay": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 4375.0
                                },
                                "net_pay": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 3400.0
                                },
                                "regular_hours": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 160.0
                                },
                                "overtime_hours": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 10.0
                                },
                                "total_hours": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 170.0
                                },
                                "federal_tax": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 600.0
                                },
                                "state_tax": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 200.0
                                },
                                "social_security": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 271.25
                                },
                                "medicare": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 63.44
                                }
                              }
                            },
                            "connection": {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "type": "integer",
                                  "example": 1
                                },
                                "name": {
                                  "type": "string",
                                  "example": "ADP Payroll Connection"
                                },
                                "provider": {
                                  "type": "string",
                                  "example": "adp"
                                }
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Paycheck not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/paychecks/{id}/download": {
      "get": {
        "tags": [
          "Paychecks"
        ],
        "summary": "Download paycheck stub",
        "description": "Get download URL for paycheck stub file (if available).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Paycheck ID",
            "schema": {
              "type": "integer",
              "example": 123
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Download information",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "download_url": {
                      "type": "string",
                      "format": "uri",
                      "example": "https://example.com/download/paycheck_123.pdf"
                    },
                    "filename": {
                      "type": "string",
                      "example": "paycheck_123_20240115.pdf"
                    },
                    "content_type": {
                      "type": "string",
                      "example": "application/pdf"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Paycheck file not available",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/pay_periods": {
      "get": {
        "tags": [
          "Pay Periods"
        ],
        "summary": "List pay periods",
        "description": "Get a list of pay periods for a specified date range.\nUseful for displaying pay period calendar and history.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "description": "Number of items per page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "start_date",
            "in": "query",
            "description": "Start date for pay period range",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2024-01-01"
            }
          },
          {
            "name": "end_date",
            "in": "query",
            "description": "End date for pay period range",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2024-12-31"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of pay periods",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "pay_periods": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "example": "2024-01-15_2024-01-21"
                          },
                          "start_date": {
                            "type": "string",
                            "format": "date",
                            "example": "2024-01-15"
                          },
                          "end_date": {
                            "type": "string",
                            "format": "date",
                            "example": "2024-01-21"
                          },
                          "period_type": {
                            "type": "string",
                            "enum": [
                              "weekly",
                              "bi-weekly",
                              "semi-monthly",
                              "monthly"
                            ],
                            "example": "weekly"
                          },
                          "days_in_period": {
                            "type": "integer",
                            "example": 7
                          },
                          "is_current": {
                            "type": "boolean",
                            "example": true
                          },
                          "is_future": {
                            "type": "boolean",
                            "example": false
                          },
                          "is_past": {
                            "type": "boolean",
                            "example": false
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 6
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Items per page",
                          "example": 25
                        }
                      },
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "No pay period configuration found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/pay_periods/current": {
      "get": {
        "tags": [
          "Pay Periods"
        ],
        "summary": "Get current pay period",
        "description": "Get detailed information about the current pay period including\nassociated timesheet, estimated pay, and completion status.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Current pay period",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "current_period": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "example": "2024-01-15_2024-01-21"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-01-15"
                        },
                        "end_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-01-21"
                        },
                        "period_type": {
                          "type": "string",
                          "enum": [
                            "weekly",
                            "bi-weekly",
                            "semi-monthly",
                            "monthly"
                          ],
                          "example": "weekly"
                        },
                        "days_in_period": {
                          "type": "integer",
                          "example": 7
                        },
                        "is_current": {
                          "type": "boolean",
                          "example": true
                        },
                        "is_future": {
                          "type": "boolean",
                          "example": false
                        },
                        "is_past": {
                          "type": "boolean",
                          "example": false
                        },
                        "payday": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-01-26"
                        },
                        "days_until_payday": {
                          "type": "integer",
                          "description": "Days until payday (for current period)",
                          "example": 5
                        },
                        "days_remaining": {
                          "type": "integer",
                          "description": "Days remaining in period (for current period)",
                          "example": 2
                        },
                        "timesheet": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 123
                            },
                            "status": {
                              "type": "string",
                              "enum": [
                                "pending",
                                "submitted",
                                "approved",
                                "rejected"
                              ],
                              "example": "pending"
                            },
                            "total_hours": {
                              "type": "number",
                              "format": "float",
                              "example": 42.5
                            },
                            "regular_hours": {
                              "type": "number",
                              "format": "float",
                              "example": 40.0
                            },
                            "overtime_hours": {
                              "type": "number",
                              "format": "float",
                              "example": 2.5
                            },
                            "submission_date": {
                              "type": "string",
                              "format": "date-time",
                              "example": "2024-01-21T17:00:00Z"
                            },
                            "approval_date": {
                              "type": "string",
                              "format": "date-time",
                              "example": "2024-01-22T09:00:00Z"
                            },
                            "editable": {
                              "type": "boolean",
                              "example": true
                            },
                            "submittable": {
                              "type": "boolean",
                              "example": true
                            }
                          }
                        },
                        "estimated_pay": {
                          "type": "number",
                          "format": "float",
                          "example": 1093.75
                        },
                        "work_days": {
                          "type": "integer",
                          "example": 5
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "No pay period configuration found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/pay_periods/next": {
      "get": {
        "tags": [
          "Pay Periods"
        ],
        "summary": "Get next pay period",
        "description": "Get information about the next pay period for planning purposes.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Next pay period",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "next_period": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "example": "2024-01-15_2024-01-21"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-01-15"
                        },
                        "end_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-01-21"
                        },
                        "period_type": {
                          "type": "string",
                          "enum": [
                            "weekly",
                            "bi-weekly",
                            "semi-monthly",
                            "monthly"
                          ],
                          "example": "weekly"
                        },
                        "days_in_period": {
                          "type": "integer",
                          "example": 7
                        },
                        "is_current": {
                          "type": "boolean",
                          "example": true
                        },
                        "is_future": {
                          "type": "boolean",
                          "example": false
                        },
                        "is_past": {
                          "type": "boolean",
                          "example": false
                        },
                        "payday": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-01-26"
                        },
                        "days_until_payday": {
                          "type": "integer",
                          "description": "Days until payday (for current period)",
                          "example": 5
                        },
                        "days_remaining": {
                          "type": "integer",
                          "description": "Days remaining in period (for current period)",
                          "example": 2
                        },
                        "timesheet": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 123
                            },
                            "status": {
                              "type": "string",
                              "enum": [
                                "pending",
                                "submitted",
                                "approved",
                                "rejected"
                              ],
                              "example": "pending"
                            },
                            "total_hours": {
                              "type": "number",
                              "format": "float",
                              "example": 42.5
                            },
                            "regular_hours": {
                              "type": "number",
                              "format": "float",
                              "example": 40.0
                            },
                            "overtime_hours": {
                              "type": "number",
                              "format": "float",
                              "example": 2.5
                            },
                            "submission_date": {
                              "type": "string",
                              "format": "date-time",
                              "example": "2024-01-21T17:00:00Z"
                            },
                            "approval_date": {
                              "type": "string",
                              "format": "date-time",
                              "example": "2024-01-22T09:00:00Z"
                            },
                            "editable": {
                              "type": "boolean",
                              "example": true
                            },
                            "submittable": {
                              "type": "boolean",
                              "example": true
                            }
                          }
                        },
                        "estimated_pay": {
                          "type": "number",
                          "format": "float",
                          "example": 1093.75
                        },
                        "work_days": {
                          "type": "integer",
                          "example": 5
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "No pay period configuration found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/pay_periods/{id}": {
      "get": {
        "tags": [
          "Pay Periods"
        ],
        "summary": "Get pay period details",
        "description": "Get detailed information about a specific pay period.\nID format: \"YYYY-MM-DD_YYYY-MM-DD\" (start_date_end_date).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Pay period ID (start_date_end_date format)",
            "schema": {
              "type": "string",
              "example": "2024-01-15_2024-01-21"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Pay period details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "pay_period": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "example": "2024-01-15_2024-01-21"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-01-15"
                        },
                        "end_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-01-21"
                        },
                        "period_type": {
                          "type": "string",
                          "enum": [
                            "weekly",
                            "bi-weekly",
                            "semi-monthly",
                            "monthly"
                          ],
                          "example": "weekly"
                        },
                        "days_in_period": {
                          "type": "integer",
                          "example": 7
                        },
                        "is_current": {
                          "type": "boolean",
                          "example": true
                        },
                        "is_future": {
                          "type": "boolean",
                          "example": false
                        },
                        "is_past": {
                          "type": "boolean",
                          "example": false
                        },
                        "payday": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-01-26"
                        },
                        "days_until_payday": {
                          "type": "integer",
                          "description": "Days until payday (for current period)",
                          "example": 5
                        },
                        "days_remaining": {
                          "type": "integer",
                          "description": "Days remaining in period (for current period)",
                          "example": 2
                        },
                        "timesheet": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 123
                            },
                            "status": {
                              "type": "string",
                              "enum": [
                                "pending",
                                "submitted",
                                "approved",
                                "rejected"
                              ],
                              "example": "pending"
                            },
                            "total_hours": {
                              "type": "number",
                              "format": "float",
                              "example": 42.5
                            },
                            "regular_hours": {
                              "type": "number",
                              "format": "float",
                              "example": 40.0
                            },
                            "overtime_hours": {
                              "type": "number",
                              "format": "float",
                              "example": 2.5
                            },
                            "submission_date": {
                              "type": "string",
                              "format": "date-time",
                              "example": "2024-01-21T17:00:00Z"
                            },
                            "approval_date": {
                              "type": "string",
                              "format": "date-time",
                              "example": "2024-01-22T09:00:00Z"
                            },
                            "editable": {
                              "type": "boolean",
                              "example": true
                            },
                            "submittable": {
                              "type": "boolean",
                              "example": true
                            }
                          }
                        },
                        "estimated_pay": {
                          "type": "number",
                          "format": "float",
                          "example": 1093.75
                        },
                        "work_days": {
                          "type": "integer",
                          "example": 5
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "400": {
            "description": "Invalid pay period ID format",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/pay_periods/{id}/summary": {
      "get": {
        "tags": [
          "Pay Periods"
        ],
        "summary": "Get pay period summary",
        "description": "Get comprehensive summary of a pay period including hours from\ndifferent sources, attendance statistics, and pay calculations.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Pay period ID (start_date_end_date format)",
            "schema": {
              "type": "string",
              "example": "2024-01-15_2024-01-21"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Pay period summary",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "period_summary": {
                      "type": "object",
                      "properties": {
                        "period": {
                          "$ref": "#/components/schemas/PayPeriod"
                        },
                        "hours": {
                          "type": "object",
                          "properties": {
                            "timesheet": {
                              "type": "object",
                              "properties": {
                                "regular": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 40.0
                                },
                                "overtime": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 2.5
                                },
                                "total": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 42.5
                                }
                              }
                            },
                            "attendance": {
                              "type": "object",
                              "properties": {
                                "regular": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 39.5
                                },
                                "overtime": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 2.0
                                },
                                "total": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 41.5
                                }
                              }
                            },
                            "scheduled": {
                              "type": "number",
                              "format": "float",
                              "example": 40.0
                            },
                            "variance": {
                              "type": "object",
                              "properties": {
                                "timesheet_vs_scheduled": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 2.5
                                },
                                "attendance_vs_scheduled": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 1.5
                                },
                                "timesheet_vs_attendance": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 1.0
                                }
                              }
                            }
                          }
                        },
                        "attendance": {
                          "type": "object",
                          "properties": {
                            "total_records": {
                              "type": "integer",
                              "example": 5
                            },
                            "completed_shifts": {
                              "type": "integer",
                              "example": 5
                            },
                            "missed_shifts": {
                              "type": "integer",
                              "example": 0
                            },
                            "late_arrivals": {
                              "type": "integer",
                              "example": 1
                            },
                            "early_departures": {
                              "type": "integer",
                              "example": 0
                            }
                          }
                        },
                        "estimated_pay": {
                          "type": "number",
                          "format": "float",
                          "example": 1093.75
                        },
                        "timesheet_status": {
                          "type": "string",
                          "enum": [
                            "not_created",
                            "pending",
                            "submitted",
                            "approved",
                            "rejected"
                          ],
                          "example": "pending"
                        },
                        "completion_percentage": {
                          "type": "number",
                          "format": "float",
                          "example": 85.7
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "400": {
            "description": "Invalid pay period ID format",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/manual_time_entries": {
      "get": {
        "tags": [
          "Manual Time Entries"
        ],
        "summary": "List manual time entries",
        "description": "Get a list of manual time entries (entries not tied to attendance records).\nThese are entries created manually by employees for missed punches or corrections.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "description": "Number of items per page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "timesheet_id",
            "in": "query",
            "description": "Filter entries for specific timesheet",
            "schema": {
              "type": "integer",
              "example": 123
            }
          },
          {
            "name": "start_date",
            "in": "query",
            "description": "Filter entries from this date",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2024-01-01"
            }
          },
          {
            "name": "end_date",
            "in": "query",
            "description": "Filter entries to this date",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2024-01-31"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by entry status",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "approved",
                "rejected"
              ],
              "example": "pending"
            }
          },
          {
            "name": "entry_type",
            "in": "query",
            "description": "Filter by entry type",
            "schema": {
              "type": "string",
              "enum": [
                "missing_punch",
                "complete",
                "overtime"
              ],
              "example": "missing_punch"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of manual time entries",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "manual_entries": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 456
                          },
                          "timesheet_id": {
                            "type": "integer",
                            "example": 123
                          },
                          "date": {
                            "type": "string",
                            "format": "date",
                            "example": "2024-01-15"
                          },
                          "start_time": {
                            "type": "string",
                            "format": "time",
                            "example": "09:00:00"
                          },
                          "end_time": {
                            "type": "string",
                            "format": "time",
                            "example": "17:00:00"
                          },
                          "hours": {
                            "type": "number",
                            "format": "float",
                            "example": 8.0
                          },
                          "entry_type": {
                            "type": "string",
                            "enum": [
                              "missing_punch",
                              "regular",
                              "overtime"
                            ],
                            "example": "missing_punch"
                          },
                          "edit_notes": {
                            "type": "string",
                            "example": "Manual entry for missed punch"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "pending",
                              "approved",
                              "rejected"
                            ],
                            "example": "pending"
                          },
                          "shift": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 789
                              },
                              "title": {
                                "type": "string",
                                "example": "Morning Shift"
                              },
                              "location": {
                                "type": "string",
                                "example": "Main Office"
                              }
                            }
                          },
                          "timesheet": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 123
                              },
                              "start_date": {
                                "type": "string",
                                "format": "date",
                                "example": "2024-01-15"
                              },
                              "end_date": {
                                "type": "string",
                                "format": "date",
                                "example": "2024-01-21"
                              },
                              "status": {
                                "type": "string",
                                "enum": [
                                  "pending",
                                  "submitted",
                                  "approved",
                                  "rejected"
                                ],
                                "example": "pending"
                              }
                            }
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2024-01-15T18:00:00Z"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2024-01-15T18:00:00Z"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 6
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Items per page",
                          "example": 25
                        }
                      },
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Manual Time Entries"
        ],
        "summary": "Create manual time entry",
        "description": "Create a new manual time entry for times not captured through\nattendance records. The associated timesheet must be editable.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "manual_time_entry"
                ],
                "properties": {
                  "manual_time_entry": {
                    "type": "object",
                    "required": [
                      "timesheet_id",
                      "date",
                      "start_time",
                      "end_time"
                    ],
                    "properties": {
                      "timesheet_id": {
                        "type": "integer",
                        "example": 123
                      },
                      "date": {
                        "type": "string",
                        "format": "date",
                        "example": "2024-01-15"
                      },
                      "start_time": {
                        "type": "string",
                        "format": "time",
                        "example": "09:00:00"
                      },
                      "end_time": {
                        "type": "string",
                        "format": "time",
                        "example": "17:00:00"
                      },
                      "hours": {
                        "type": "number",
                        "format": "float",
                        "description": "Hours worked (calculated if not provided)",
                        "example": 8.0
                      },
                      "edit_notes": {
                        "type": "string",
                        "example": "Manual entry for missed punch"
                      },
                      "shift_id": {
                        "type": "integer",
                        "description": "Associated shift ID (optional)",
                        "example": 456
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Manual time entry created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "manual_entry": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ManualTimeEntry"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "edit_history": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 1
                                  },
                                  "field_changed": {
                                    "type": "string",
                                    "example": "end_time"
                                  },
                                  "original_value": {
                                    "type": "string",
                                    "example": "16:30:00"
                                  },
                                  "new_value": {
                                    "type": "string",
                                    "example": "17:00:00"
                                  },
                                  "reason": {
                                    "type": "string",
                                    "example": "Employee requested time correction"
                                  },
                                  "edited_by": {
                                    "type": "string",
                                    "example": "John Doe"
                                  },
                                  "edited_at": {
                                    "type": "string",
                                    "format": "date-time",
                                    "example": "2024-01-16T09:00:00Z"
                                  }
                                }
                              }
                            },
                            "validation_warnings": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "type": {
                                    "type": "string",
                                    "enum": [
                                      "overlap",
                                      "excessive_hours",
                                      "weekend_work"
                                    ],
                                    "example": "overlap"
                                  },
                                  "message": {
                                    "type": "string",
                                    "example": "This entry overlaps with another time entry on the same date"
                                  }
                                }
                              }
                            },
                            "pay_calculation": {
                              "type": "object",
                              "properties": {
                                "regular_hours": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 8.0
                                },
                                "overtime_hours": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 0.0
                                },
                                "regular_pay": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 200.0
                                },
                                "overtime_pay": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 0.0
                                },
                                "total_pay": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 200.0
                                },
                                "hourly_rate": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 25.0
                                },
                                "overtime_rate": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 37.5
                                }
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Validation failed or timesheet not editable",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/manual_time_entries/{id}": {
      "get": {
        "tags": [
          "Manual Time Entries"
        ],
        "summary": "Get manual time entry details",
        "description": "Get detailed information about a specific manual time entry including\nedit history, validation warnings, and pay calculations.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Manual time entry ID",
            "schema": {
              "type": "integer",
              "example": 456
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Manual time entry details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "manual_entry": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ManualTimeEntry"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "edit_history": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 1
                                  },
                                  "field_changed": {
                                    "type": "string",
                                    "example": "end_time"
                                  },
                                  "original_value": {
                                    "type": "string",
                                    "example": "16:30:00"
                                  },
                                  "new_value": {
                                    "type": "string",
                                    "example": "17:00:00"
                                  },
                                  "reason": {
                                    "type": "string",
                                    "example": "Employee requested time correction"
                                  },
                                  "edited_by": {
                                    "type": "string",
                                    "example": "John Doe"
                                  },
                                  "edited_at": {
                                    "type": "string",
                                    "format": "date-time",
                                    "example": "2024-01-16T09:00:00Z"
                                  }
                                }
                              }
                            },
                            "validation_warnings": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "type": {
                                    "type": "string",
                                    "enum": [
                                      "overlap",
                                      "excessive_hours",
                                      "weekend_work"
                                    ],
                                    "example": "overlap"
                                  },
                                  "message": {
                                    "type": "string",
                                    "example": "This entry overlaps with another time entry on the same date"
                                  }
                                }
                              }
                            },
                            "pay_calculation": {
                              "type": "object",
                              "properties": {
                                "regular_hours": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 8.0
                                },
                                "overtime_hours": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 0.0
                                },
                                "regular_pay": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 200.0
                                },
                                "overtime_pay": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 0.0
                                },
                                "total_pay": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 200.0
                                },
                                "hourly_rate": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 25.0
                                },
                                "overtime_rate": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 37.5
                                }
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Manual time entry not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Manual Time Entries"
        ],
        "summary": "Update manual time entry",
        "description": "Update a manual time entry with new times or notes. The associated\ntimesheet must be editable.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Manual time entry ID",
            "schema": {
              "type": "integer",
              "example": 456
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "manual_time_entry"
                ],
                "properties": {
                  "manual_time_entry": {
                    "type": "object",
                    "properties": {
                      "date": {
                        "type": "string",
                        "format": "date",
                        "example": "2024-01-15"
                      },
                      "start_time": {
                        "type": "string",
                        "format": "time",
                        "example": "09:00:00"
                      },
                      "end_time": {
                        "type": "string",
                        "format": "time",
                        "example": "17:00:00"
                      },
                      "hours": {
                        "type": "number",
                        "format": "float",
                        "example": 8.0
                      },
                      "edit_notes": {
                        "type": "string",
                        "example": "Corrected end time"
                      },
                      "shift_id": {
                        "type": "integer",
                        "example": 456
                      }
                    }
                  },
                  "edit_reason": {
                    "type": "string",
                    "description": "Reason for the edit (for audit trail)",
                    "example": "Employee requested time correction"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Manual time entry updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "manual_entry": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ManualTimeEntry"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "edit_history": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 1
                                  },
                                  "field_changed": {
                                    "type": "string",
                                    "example": "end_time"
                                  },
                                  "original_value": {
                                    "type": "string",
                                    "example": "16:30:00"
                                  },
                                  "new_value": {
                                    "type": "string",
                                    "example": "17:00:00"
                                  },
                                  "reason": {
                                    "type": "string",
                                    "example": "Employee requested time correction"
                                  },
                                  "edited_by": {
                                    "type": "string",
                                    "example": "John Doe"
                                  },
                                  "edited_at": {
                                    "type": "string",
                                    "format": "date-time",
                                    "example": "2024-01-16T09:00:00Z"
                                  }
                                }
                              }
                            },
                            "validation_warnings": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "type": {
                                    "type": "string",
                                    "enum": [
                                      "overlap",
                                      "excessive_hours",
                                      "weekend_work"
                                    ],
                                    "example": "overlap"
                                  },
                                  "message": {
                                    "type": "string",
                                    "example": "This entry overlaps with another time entry on the same date"
                                  }
                                }
                              }
                            },
                            "pay_calculation": {
                              "type": "object",
                              "properties": {
                                "regular_hours": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 8.0
                                },
                                "overtime_hours": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 0.0
                                },
                                "regular_pay": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 200.0
                                },
                                "overtime_pay": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 0.0
                                },
                                "total_pay": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 200.0
                                },
                                "hourly_rate": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 25.0
                                },
                                "overtime_rate": {
                                  "type": "number",
                                  "format": "float",
                                  "example": 37.5
                                }
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Update failed or timesheet not editable",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Manual Time Entries"
        ],
        "summary": "Delete manual time entry",
        "description": "Delete a manual time entry. Only entries not tied to attendance\nrecords can be deleted, and the timesheet must be editable.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Manual time entry ID",
            "schema": {
              "type": "integer",
              "example": 456
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Manual time entry deleted successfully"
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Cannot delete entry (tied to attendance or timesheet not editable)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/service_desk": {
      "get": {
        "tags": [
          "Service Desk"
        ],
        "summary": "List support tickets",
        "description": "Retrieve all SERVICE DESK SUPPORT TICKETS and HELP REQUESTS for the current user (NOT forms or form templates). Returns both tickets created by the user (as requester) and tickets assigned to the user (as assignee).\n\nUse this endpoint to:\n- List all my support tickets\n- Show my help requests  \n- View my IT tickets\n- Get support request list\n- See incident reports\n- Check service desk tickets\n- Find support tickets by status or priority\n\nSupports filtering by status and priority. ⚠️ This is for SUPPORT TICKETS only, not for forms.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "Filter by ticket status",
            "schema": {
              "type": "string",
              "enum": [
                "submitted",
                "assigned",
                "in_progress",
                "resolved",
                "closed"
              ]
            }
          },
          {
            "name": "priority",
            "in": "query",
            "description": "Filter by priority level",
            "schema": {
              "type": "string",
              "enum": [
                "low",
                "medium",
                "high",
                "critical"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of tickets retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "count": {
                      "type": "integer",
                      "example": 5
                    },
                    "tickets": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SupportTicketSummary"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Create support ticket",
        "description": "Create a new support ticket or help request. Use this to create ticket, submit request, report issue, request help, open ticket, file support request, log incident, create help request, or submit support ticket.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ticket"
                ],
                "properties": {
                  "ticket": {
                    "type": "object",
                    "required": [
                      "title",
                      "description"
                    ],
                    "properties": {
                      "title": {
                        "type": "string",
                        "description": "Brief title of the issue",
                        "example": "Laptop overheating issue"
                      },
                      "description": {
                        "type": "string",
                        "description": "Detailed description of the problem",
                        "example": "My laptop has been overheating when running multiple applications"
                      },
                      "request_type": {
                        "type": "string",
                        "enum": [
                          "general",
                          "it_support",
                          "hr_support",
                          "facilities",
                          "equipment",
                          "training",
                          "access",
                          "travel_expense",
                          "health_safety",
                          "payroll_benefits"
                        ],
                        "example": "it_support"
                      },
                      "priority": {
                        "type": "string",
                        "enum": [
                          "low",
                          "medium",
                          "high",
                          "critical"
                        ],
                        "example": "medium"
                      },
                      "location_id": {
                        "type": "integer",
                        "nullable": true,
                        "example": 1
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Ticket created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Support ticket created successfully"
                    },
                    "ticket": {
                      "$ref": "#/components/schemas/SupportTicketDetail"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/service_desk/search": {
      "get": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Search support tickets",
        "description": "Search and filter SERVICE DESK SUPPORT TICKETS with advanced query capabilities (NOT for forms). Supports full-text search across ticket title, description, and resolution notes with pagination and sorting.\n\nUse this endpoint to:\n- Search support tickets by keyword\n- Find help requests\n- Filter tickets by status or priority\n- Find specific support issues\n- Search IT ticket history\n- Look for related support tickets\n- Find tickets by type (IT, HR, facilities, etc.)\n\n⚠️ This searches SUPPORT TICKETS only, not forms or form submissions.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "query",
            "in": "query",
            "description": "Search term (searches title, description, resolution notes)",
            "schema": {
              "type": "string",
              "example": "laptop"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by status",
            "schema": {
              "type": "string",
              "enum": [
                "submitted",
                "assigned",
                "in_progress",
                "resolved",
                "closed"
              ]
            }
          },
          {
            "name": "priority",
            "in": "query",
            "description": "Filter by priority",
            "schema": {
              "type": "string",
              "enum": [
                "low",
                "medium",
                "high",
                "critical"
              ]
            }
          },
          {
            "name": "request_type",
            "in": "query",
            "description": "Filter by request type",
            "schema": {
              "type": "string",
              "example": "it_support"
            }
          },
          {
            "name": "created_after",
            "in": "query",
            "description": "Filter tickets created after this date",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "created_before",
            "in": "query",
            "description": "Filter tickets created before this date",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "sort_by",
            "in": "query",
            "description": "Field to sort by",
            "schema": {
              "type": "string",
              "default": "created_at",
              "enum": [
                "created_at",
                "updated_at",
                "priority",
                "status"
              ]
            }
          },
          {
            "name": "sort_order",
            "in": "query",
            "description": "Sort direction",
            "schema": {
              "type": "string",
              "default": "desc",
              "enum": [
                "asc",
                "desc"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Results per page (max 100)",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Search results with pagination",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "page": {
                      "type": "integer"
                    },
                    "per_page": {
                      "type": "integer"
                    },
                    "total_pages": {
                      "type": "integer"
                    },
                    "tickets": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SupportTicketSummary"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/service_desk/service_catalog": {
      "get": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Browse service catalog",
        "description": "Browse the service catalog to see available request types for submitting\nhelp requests. Returns all enabled service types organized by popularity.\nSupports search and category filtering.\n\nAccessible to all authenticated users (not admin-only).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "description": "Search term to filter service types by title, description, or examples",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "category",
            "in": "query",
            "description": "Filter by category name",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "department_id",
            "in": "query",
            "description": "Filter by service department ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Service catalog retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "service_types": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ServiceCatalogType"
                      }
                    },
                    "popular_types": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ServiceCatalogType"
                      }
                    },
                    "categories": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          }
        }
      }
    },
    "/service_desk/kb/search": {
      "get": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Search the knowledge base",
        "description": "Search the service desk knowledge base (help articles, FAQs, documents)\nfor self-service answers. Accessible to all authenticated members.\n\nUses the same hybrid search the Ask AI service desk agent uses, with\nthe same role-based visibility filtering — callers only see articles\ntheir role permits. Results are capped at 10 and never cached.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "Search query",
            "schema": {
              "type": "string",
              "example": "VPN password reset"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum results (default 10, max 10)",
            "schema": {
              "type": "integer",
              "default": 10,
              "minimum": 1,
              "maximum": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Search results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "query": {
                      "type": "string"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/KbSearchResult"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing search query"
          }
        }
      }
    },
    "/service_desk/{id}": {
      "get": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Get support ticket details",
        "description": "Retrieve detailed information about a specific SERVICE DESK SUPPORT TICKET or HELP REQUEST (NOT a form). This endpoint returns support ticket status, assigned agent, priority level, complete description, comment history, and resolution timeline.\n\nUse this endpoint to:\n- Check support ticket status (\"What's the status of ticket #70?\")\n- View help request details (\"Show me ticket 70\")\n- See who is handling a support ticket\n- Get IT support ticket information  \n- View incident report status\n- Check service desk request progress\n- Find out about a specific support request or help ticket\n\n⚠️ IMPORTANT: This is for SUPPORT TICKETS only, not for forms, templates, or form submissions. Ticket IDs are from the Service Desk system, not the Forms app.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Ticket ID number",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ticket details retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "ticket": {
                      "$ref": "#/components/schemas/SupportTicketDetail"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Ticket not found or access denied",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Update support ticket",
        "description": "Update the editable fields of an existing support ticket.\n\nField access is role-based:\n- Requesters may update **title** and **description** on their own ticket,\n  only while the ticket is still editable (status submitted, queued, or assigned).\n- Privileged callers (business admins, super admins, help desk agents,\n  or the current assignee) may additionally update **priority**,\n  **request_type**, and **service_department_id**.\n\nAdditional rules:\n- No one can edit resolved, closed, cancelled, or rejected tickets.\n- Priority changes also respect the business priority source mode\n  (in \"system\"/\"agent\" modes only service desk staff may set priority)\n  and the prevent-self-service-on-own-tickets setting.\n- **Status cannot be changed here** — use PATCH /service_desk/{id}/status.\n- Changing request_type may move the ticket to pending_approval if the\n  new type requires approval.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Ticket ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ticket"
                ],
                "properties": {
                  "ticket": {
                    "type": "object",
                    "properties": {
                      "title": {
                        "type": "string",
                        "example": "Laptop overheating issue (updated)"
                      },
                      "description": {
                        "type": "string",
                        "example": "Updated description with more details"
                      },
                      "priority": {
                        "type": "string",
                        "enum": [
                          "low",
                          "medium",
                          "high",
                          "critical"
                        ],
                        "description": "Privileged callers only; also gated by priority source mode"
                      },
                      "request_type": {
                        "type": "string",
                        "description": "Privileged callers only",
                        "example": "it_support"
                      },
                      "service_department_id": {
                        "type": "integer",
                        "nullable": true,
                        "description": "Privileged callers only"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Ticket updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    },
                    "ticket": {
                      "$ref": "#/components/schemas/SupportTicketDetail"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not permitted to edit this ticket (or this field)"
          },
          "404": {
            "description": "Ticket not found or access denied"
          },
          "422": {
            "description": "Validation error"
          }
        }
      }
    },
    "/service_desk/{id}/assign": {
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Assign or transfer ticket",
        "description": "Assign a support ticket to a user, transfer it to another user, or move\nit into a team queue. Privileged only: business admins, super admins,\nmanagers, help desk agents, or the ticket's current assignee.\n\nRules:\n- Closed, cancelled, or rejected tickets cannot be assigned/transferred.\n- Tickets pending approval must be approved or rejected first.\n- When prevent-self-service is enabled, the requester cannot assign\n  their own ticket to themselves.\n- Fires the same notifications and milestone tracking as the web app.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Ticket ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "target_type": {
                    "type": "string",
                    "enum": [
                      "user",
                      "team"
                    ],
                    "default": "user",
                    "description": "Assign to an individual user or transfer to a team queue"
                  },
                  "assignee_id": {
                    "type": "integer",
                    "description": "Required when target_type is \"user\"",
                    "example": 42
                  },
                  "support_team_id": {
                    "type": "integer",
                    "description": "Required when target_type is \"team\"",
                    "example": 3
                  },
                  "transfer_reason": {
                    "type": "string",
                    "description": "Optional reason recorded on transfer",
                    "example": "Better suited for the networking team"
                  },
                  "notify_assignee": {
                    "type": "boolean",
                    "default": true,
                    "description": "Send the assignment/transfer notification (default true)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Ticket assigned or transferred successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    },
                    "ticket": {
                      "$ref": "#/components/schemas/SupportTicketDetail"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not authorized to assign or transfer this ticket"
          },
          "404": {
            "description": "Ticket, assignee, or team not found"
          },
          "422": {
            "description": "Ticket state does not allow assignment, or missing target"
          }
        }
      }
    },
    "/service_desk/{id}/comments": {
      "get": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Get ticket comments",
        "description": "Retrieve all comments and updates on a support ticket. Use this to view ticket comments, see conversation history, check what was discussed, read agent responses, view ticket updates, or see comment thread.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Ticket ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of comments retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "ticket_id": {
                      "type": "integer"
                    },
                    "comments_count": {
                      "type": "integer"
                    },
                    "comments": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TicketComment"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Ticket not found or access denied"
          }
        }
      },
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Add comment to ticket",
        "description": "Add a comment or update to an existing support ticket. Use this to add comment, update ticket, provide more information, respond to agent, add details, reply to ticket, post update, or add note to ticket.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Ticket ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "comment"
                ],
                "properties": {
                  "comment": {
                    "type": "object",
                    "required": [
                      "content"
                    ],
                    "properties": {
                      "content": {
                        "type": "string",
                        "description": "Comment text",
                        "example": "I've restarted the laptop and the issue persists"
                      },
                      "internal": {
                        "type": "boolean",
                        "description": "Internal note (not visible to requester)",
                        "default": false
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Comment added successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    },
                    "comment": {
                      "$ref": "#/components/schemas/TicketComment"
                    },
                    "ticket": {
                      "$ref": "#/components/schemas/SupportTicketDetail"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Ticket not found"
          },
          "422": {
            "description": "Validation error"
          }
        }
      }
    },
    "/service_desk/{id}/status": {
      "patch": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Update ticket status",
        "description": "Change the status of a support ticket (resolve, close, reopen, mark in progress). Use this to close ticket, resolve ticket, reopen ticket, mark in progress, update status, change ticket state, mark resolved, mark closed, or start work on ticket.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Ticket ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "action_type"
                ],
                "properties": {
                  "action_type": {
                    "type": "string",
                    "description": "Status action to perform",
                    "enum": [
                      "resolve",
                      "close",
                      "reopen",
                      "in_progress"
                    ],
                    "example": "resolve"
                  },
                  "notes": {
                    "type": "string",
                    "description": "Optional notes about status change (resolution notes, closing reason, etc.)",
                    "example": "Issue was fixed by restarting the VPN service"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Status updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    },
                    "ticket": {
                      "$ref": "#/components/schemas/SupportTicketDetail"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid action type"
          },
          "403": {
            "description": "Not authorized to perform this action"
          },
          "404": {
            "description": "Ticket not found"
          },
          "422": {
            "description": "Status update failed"
          }
        }
      }
    },
    "/ask_ai/messages": {
      "post": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Send message to AI",
        "description": "Send a message to the AI assistant and initiate streaming response.\n\n**Conversation Restoration:**\nIf no `conversation_id` is provided, the API will automatically try to restore\nthe user's most recent active conversation. This ensures conversation history\npersists across logout/login cycles without requiring the client to store\nconversation IDs.\n\nThe response includes a `conversation_id` that the client should use to:\n1. Subscribe to the `AiResponseChannel` WebSocket for streaming responses\n2. Reference this conversation in subsequent requests\n\n**WebSocket Subscription:**\n```javascript\nconst channel = consumer.subscriptions.create(\n  { channel: \"AiResponseChannel\", conversation_id: response.conversation_id },\n  {\n    received(data) {\n      switch(data.type) {\n        case 'chunk': // Streaming text chunk\n        case 'complete': // Full response with metadata\n        case 'error': // Error occurred\n        case 'status': // Status update (thinking, generating)\n      }\n    }\n  }\n);\n```\n\nUse this endpoint to:\n- Ask questions about schedules, PTO, policies\n- Get help with IT issues\n- Request information from company knowledge base\n- Perform actions like submitting time off requests\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "message"
                ],
                "properties": {
                  "message": {
                    "type": "string",
                    "description": "The user's message to the AI",
                    "example": "How many PTO days do I have left?"
                  },
                  "conversation_id": {
                    "deprecated": true,
                    "type": "string",
                    "format": "uuid",
                    "description": "**Accepted for backward compatibility but IGNORED.** The server always\nresolves the thread itself — it continues the caller's most recent\nactive conversation, or mints a new id when there is none. Sending a\nvalue here changes nothing except a log line.\n\nThis is deliberate, and it is a security fix rather than an oversight:\nthe resolved id becomes the key of the WebSocket authorization record\nthat `AiResponseChannel` reads to decide who may subscribe to a stream,\nso honouring a client-supplied value would let any authenticated user\ntake over another user's stream.\n\nClients MUST read the `conversation_id` returned in the response (and\n`websocket.subscription.conversation_id`, which is the same value) and\nsubscribe with THAT — never with an id they generated or cached.\n",
                    "example": "550e8400-e29b-41d4-a716-446655440000"
                  },
                  "mode": {
                    "deprecated": true,
                    "type": "string",
                    "description": "**Accepted and validated, but currently has NO effect.** Nothing on the\nstreaming path reads it — `AskAiStreamingJob` forwards `system_context`,\n`ask_ai_context`, `conversation_history` and `page_context` to the agent\npipeline and never `mode` — so every value routes exactly like `general`.\nAn unrecognised value is silently treated as `general` rather than\nrejected.\n\nKept in the contract because existing native builds send it and it is\nthe seam a future forward would use; do not build client behaviour on\nthe assumption that it changes routing.\n",
                    "enum": [
                      "general",
                      "help",
                      "scheduling"
                    ],
                    "default": "general"
                  },
                  "system_context": {
                    "type": "string",
                    "description": "Optional additional context for the AI (hidden from user)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Message accepted, streaming initiated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResponse"
                }
              }
            }
          },
          "403": {
            "description": "Ask AI is not enabled for this business or not visible to this user\n(`app_disabled`), or the API token lacks the `write:chat` scope\n(`insufficient_permissions`). Full-access and `admin` tokens, and\nsession-authenticated callers, are unaffected.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "422": {
            "description": "Invalid request (missing message)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "503": {
            "description": "AI service unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/ask_ai/conversations/{id}": {
      "get": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Get conversation details",
        "description": "Retrieve metadata about a specific conversation including status and message count.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Conversation session ID (UUID)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Conversation details retrieved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "conversation": {
                      "$ref": "#/components/schemas/Conversation"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Conversation not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Clear conversation",
        "description": "Clear conversation history. By default, only clears messages.\nUse `clear_everything=true` to also clear learned facts and cache.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Conversation session ID (UUID)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "clear_everything",
            "in": "query",
            "description": "If true, clears all memory including learned facts",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Conversation cleared successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Conversation cleared"
                    },
                    "cleared": {
                      "type": "object",
                      "properties": {
                        "messages": {
                          "type": "boolean"
                        },
                        "facts": {
                          "type": "boolean"
                        },
                        "cache": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Conversation not found"
          }
        }
      }
    },
    "/ask_ai/conversations/{id}/messages": {
      "get": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Get conversation messages",
        "description": "Retrieve paginated message history for a conversation.\nMessages are returned in chronological order (oldest first).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Conversation session ID (UUID)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number (1-based)",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Messages per page (max 100)",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Optional search term (max 200 chars). Restricts the result to messages\nmatching it, using the same full-text + substring match the web History\npage uses. `pagination.total_count` and `total_pages` describe the\nFILTERED set, so paging works unchanged while a search is active.\n",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Messages retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "conversation_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "messages": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Message"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Conversation not found"
          }
        }
      }
    },
    "/ask_ai/help": {
      "get": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Get AI capabilities and examples",
        "description": "Get list of available AI agents with their capabilities and example questions.\nUse this to show users what they can ask the AI assistant.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Help examples retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "agents": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AgentInfo"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/ask_ai/cancel": {
      "post": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Cancel active AI request",
        "description": "Cancel an in-progress AI request for a conversation.\nSends a cancellation signal via WebSocket.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "conversation_id"
                ],
                "properties": {
                  "conversation_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The conversation to cancel",
                    "example": "550e8400-e29b-41d4-a716-446655440000"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cancellation signal sent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Cancellation signal sent"
                    },
                    "conversation_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Conversation not found"
          },
          "422": {
            "description": "Invalid request (missing conversation_id)"
          }
        }
      }
    },
    "/ask_ai/settings": {
      "get": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Get AI feature settings and current conversation",
        "description": "Get feature settings for the mobile client to control UI behavior.\n\n**Also returns the user's current/latest active conversation** to support\nsession restoration after logout/login. Mobile apps should call this endpoint\non startup and use the returned `current_conversation.id` to restore the\nuser's previous conversation.\n\nReturns minimal feature toggles including:\n- `create_support_ticket_enabled`: Whether \"Create Support Ticket\" button should be shown\n- `function_calls_enabled`: Whether AI can execute actions on behalf of the user\n- `schedule_queries_enabled`: Whether scheduling-related queries are available\n- `general_questions_enabled`: Whether general Q&A is enabled\n- `voice_mode_enabled`: Whether voice mode is available (check before showing voice button)\n- `voice_mode_limit_info`: Usage limits for voice mode (daily/session limits, remaining minutes)\n\n**Voice Mode Check:**\nAlways check `voice_mode_enabled` before showing voice UI. If enabled, \n`voice_mode_limit_info` provides usage data to display to users.\n\n**Session Restoration:**\nThe `current_conversation` field contains the user's most recent active conversation,\nor `null` if no active conversation exists. Use this to restore conversation history\nafter the user logs out and logs back in.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Settings retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AskAiSettings"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          }
        }
      }
    },
    "/ask_ai/voice_token": {
      "post": {
        "tags": [
          "Ask AI",
          "Voice Mode"
        ],
        "summary": "Get voice mode token",
        "description": "Generate an ephemeral token for OpenAI Realtime API voice mode.\n\n**Voice Mode Architecture:**\nVoice mode uses a dual-connection approach:\n1. **OpenAI WebRTC**: Direct audio streaming between mobile app and OpenAI\n2. **ActionCable WebSocket**: Backend queries and response handling\n\n**Flow:**\n1. Call this endpoint to get ephemeral token and session configuration\n2. Connect to OpenAI via WebRTC using the ephemeral token\n3. Connect to ActionCable and subscribe to `VoiceRealtimeChannel`\n4. User speaks → OpenAI transcribes → Send transcript to ActionCable\n5. Backend processes query → Response sent via ActionCable\n6. Send response text to OpenAI for TTS → Audio plays to user\n\n**Rate Limiting:**\n- 30 minutes per user per day\n- 15 minutes max per session\n\nSee the [Mobile Voice Mode Guide](/api-docs/guides/ask-ai-mobile.md#voice-mode-integration) \nfor detailed implementation instructions.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "voice": {
                    "type": "string",
                    "description": "OpenAI voice to use for TTS. Validated against the full supported set\n(`OpenaiTtsService::VOICES`); an unrecognised value falls back to the\nbusiness's configured \"AI Voice\" setting, or to `marin` when none is set,\nrather than erroring.\n",
                    "enum": [
                      "alloy",
                      "ash",
                      "ballad",
                      "coral",
                      "echo",
                      "fable",
                      "nova",
                      "onyx",
                      "sage",
                      "shimmer",
                      "verse",
                      "marin",
                      "cedar"
                    ],
                    "default": "marin"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Voice token generated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VoiceTokenResponse"
                }
              }
            }
          },
          "402": {
            "description": "Insufficient voice minutes balance",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VoiceErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Voice mode is not enabled for this business (`feature_disabled`), Ask AI is\nnot enabled/visible for this user (`app_disabled`), or the API token lacks\nthe `write:chat` scope (`insufficient_permissions`). Full-access and `admin`\ntokens, and session-authenticated callers, are unaffected.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Daily voice usage limit reached (`voice_limit_reached`), or the per-user\nhourly mint cap was exceeded (`rate_limited`). `error.details` carries the\nusage meter (`daily_used`, `daily_limit`, `remaining`, `session_limit`) plus\n`open_sessions` / `reserved_minutes` — the minutes already committed by\nsessions this user has not ended, which count toward the daily cap even\nthough no duration has been recorded for them yet.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "503": {
            "description": "AI service not configured",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/ask_ai/voice/sessions/{session_id}": {
      "get": {
        "tags": [
          "Ask AI",
          "Voice Mode"
        ],
        "summary": "Get voice session status",
        "description": "Check the status of a voice session. Useful for reconnection handling\nwhen the mobile app needs to verify if a session is still active.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "session_id",
            "in": "path",
            "required": true,
            "description": "Voice session ID (UUID returned from voice_token endpoint)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Session status retrieved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VoiceSessionStatus"
                }
              }
            }
          },
          "404": {
            "description": "Session not found or doesn't belong to current user"
          }
        }
      }
    },
    "/ask_ai/voice/sessions/{session_id}/end": {
      "post": {
        "tags": [
          "Ask AI",
          "Voice Mode"
        ],
        "summary": "End voice session",
        "description": "Explicitly end a voice session for graceful cleanup and billing finalization.\n\n**Important:** Always call this endpoint when ending a voice session to ensure\nproper billing and cleanup, even if the WebSocket connection was lost unexpectedly.\n\nThis endpoint:\n1. Calculates final session duration\n2. Charges the appropriate billing amount\n3. Cleans up session data from cache\n\nIf the session was already ended (via ActionCable or timeout), this endpoint\nreturns success with the already-recorded billing information.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "session_id",
            "in": "path",
            "required": true,
            "description": "Voice session ID (UUID returned from voice_token endpoint)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Session ended successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VoiceSessionEndResponse"
                }
              }
            }
          },
          "404": {
            "description": "Session not found or doesn't belong to current user"
          }
        }
      }
    },
    "/notifications": {
      "get": {
        "tags": [
          "Notifications"
        ],
        "summary": "List notifications",
        "description": "Retrieve notifications for the current user. Returns paginated notifications\nscoped to the current business.\n\nUse this endpoint to:\n- List all active notifications\n- Filter by read/unread/archived status\n- Filter to ONE activity type (Wikis, Announcements, Messages, …)\n- Paginate through notification history\n\n**`filter` carries two vocabularies.**\n\n*Read state* — the values that shipped first, unchanged:\n\n- `unread` — only unread notifications\n- `read` — only read (non-archived) notifications\n- `archived` — only archived notifications\n- `all` (or omit) — all active (non-archived) notifications\n\n*Activity subject* — one activity type. The value is a subject `key`\nexactly as `GET /notifications/home` hands it out on any Activity\nsection or Needs-you entry (`wikis`, `system_announcements`,\n`broadcast_messages`, `direct_messages`, `training`, `forms`,\n`news_feed_notifications`, `kind:dm`, `kind:broadcast`, …). This is what\nthe Activity group's **See all →** opens, and the same key the web\n\"See all wikis\" link puts in `?subject=`.\n\n**Every registered notification category is a valid value**, plus the\nfour `kind:` fallback buckets (`kind:broadcast`, `kind:agent`,\n`kind:dm`, `kind:system`) that an uncategorised notification lands in —\nnot merely the subjects that happen to have rows right now. The set is\nderived from the category registry, so it grows on its own as apps are\nadded and never needs a client release.\n\nPass back the key you were given rather than hardcoding a list. The\nsubjects one user sees are a small slice of the whole set — a user with\nno schedule notifications never sees `schedule_management` on their own\nhome screen, while most of their colleagues do — so a hardcoded list\nbuilt from one account's inbox will be wrong for everyone else.\n\nA valid key you have no notifications in returns an empty `200`, not an\nerror. Matching is case- and separator-insensitive **over the key**:\n`system_announcements`, `system-announcements` and\n`System Announcements` are one value. It does not match display\n**labels** — the label of `news_feed_notifications` is \"News Feed\", and\n`filter=News Feed` is a `400` whose message points at the real key. Pass\nback the `key`, not the `label`.\n\nRead states (`unread`, `read`, `archived`, `all`) are matched the same\nway, so `filter=Unread` and `filter=unread` are one value too.\n\nA subject filter lists that subject across the **visible inbox** —\nsnoozed, expired and archived rows are excluded, the same visibility\n`GET /notifications/home` counts under.\n\nIt covers **both zones**: a subject can hold open asks as well as\nactivity, and the key you pass back may have come from either section, so\nthe list holds that subject's `activity.sections[].count` **plus** its\n`needs_you.entries[].count`. Expect a list longer than the Activity chip\nalone whenever the subject also has something waiting on the user; the\nrows carry `action_required` / `action_completed_at`, so a client that\nwants one zone can split them itself.\n\nAdd `folder=` to narrow it further (`folder=unread&filter=wikis`)\nor `q=` to search within it.\n\n`subject=<key>` is the older spelling of the same narrowing and still\nworks. Passing both is fine when they agree; passing two DIFFERENT\nsubjects is a `400 conflicting_subject_filter` rather than a silently\ndropped filter.\n\nA `filter` value that is neither a read state nor a known subject key is\na `400 invalid_filter` — the endpoint will not answer \"wikis\" with every\nnotification you have. The error message suggests near-miss keys.\n\n**Ordering.** Every folder is returned in the same order the web Inbox\nuses, so a native client can render the list as-is and match what the\nuser sees on the web. There are two rules.\n\n*Archived* is ordered by `archived_at` descending — most recently\nARCHIVED first, nulls last. The archive is a filing cabinet, so the item\nthe user just archived is on top regardless of how old the underlying\nevent is. Ties fall back to `created_at` then `id`, both descending (a\nbulk archive stamps one identical `archived_at` across the whole\nselection, so the tiebreak is routine).\n\n*Every other folder* is ordered by triage:\n\n1. open action requests first (`action_required` and not yet completed)\n2. then `priority`, high to low\n3. then `created_at`, newest first\n4. then `id` descending, so paging can't repeat or skip a row that ties\n   another on `created_at`\n\nClients should render in the order received rather than re-sorting.\n`created_at` is only a tiebreaker in both rules, and sorting the archive\nby it reproduces the bug this ordering exists to fix. Unarchiving does\nnot reorder anything: `archived_at` is cleared and the item returns to\nits ranked position in the active list.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "filter",
            "in": "query",
            "description": "Read state (`unread`, `read`, `archived`, `all`) OR an activity subject key from `GET /notifications/home`. Not an enum: the subject half is derived from the tenant's registered notification categories and grows with the apps installed. See the endpoint description.",
            "schema": {
              "type": "string"
            },
            "examples": {
              "unread": {
                "summary": "Read state — unread only",
                "value": "unread"
              },
              "wikis": {
                "summary": "Activity type — Wikis",
                "value": "wikis"
              },
              "announcements": {
                "summary": "Activity type — System Announcements",
                "value": "system_announcements"
              },
              "messages": {
                "summary": "Activity type — Direct Messages",
                "value": "direct_messages"
              }
            }
          },
          {
            "name": "subject",
            "in": "query",
            "description": "Narrow to one activity subject — the older spelling of `filter=<key>`, and what the web \"See all wikis\" link uses. Unlike `filter`, an unrecognised key returns an empty list rather than an error.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "folder",
            "in": "query",
            "description": "Inbox folder to list. Composes with `filter=<subject>` / `subject=`. `messages` is accepted but served by the chat API, so it returns an empty list here — read mail through the chat endpoints. `mail-alerts` is the folder that lists the notifications ABOUT mail.",
            "schema": {
              "type": "string",
              "enum": [
                "inbox",
                "unread",
                "read",
                "action",
                "broadcasts",
                "announcements",
                "system",
                "agents",
                "snoozed",
                "archived",
                "mail-alerts",
                "messages"
              ]
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Free-text search over notification title and content.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number (1-indexed)",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Items per page (max 100)",
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of notifications",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "notifications",
                    "meta"
                  ],
                  "properties": {
                    "notifications": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Notification"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_filter` — the `filter` value is neither a read state nor a known activity subject key. Or `conflicting_subject_filter` — `filter` and `subject` named two different subjects.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/notifications/{id}": {
      "get": {
        "tags": [
          "Notifications"
        ],
        "summary": "Get a notification",
        "description": "Retrieve a single notification by ID. The notification must belong to the\ncurrent user and current business.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Notification ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Notification details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "notification"
                  ],
                  "properties": {
                    "notification": {
                      "$ref": "#/components/schemas/Notification"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Notification not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "notification_not_found"
                        },
                        "message": {
                          "type": "string",
                          "example": "Notification not found"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/notifications/unread_count": {
      "get": {
        "tags": [
          "Notifications"
        ],
        "summary": "Get unread notification count",
        "description": "Returns the count of unread notifications for the current user\nwithin the current business. Useful for badge counts on mobile.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Unread count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "count"
                  ],
                  "properties": {
                    "count": {
                      "type": "integer",
                      "description": "Number of unread notifications",
                      "example": 5
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    },
    "/notifications/home": {
      "get": {
        "tags": [
          "Notifications"
        ],
        "summary": "Notifications home screen (Needs you + Activity)",
        "description": "Everything needed to draw the notifications front door — the screen behind\nthe bell icon — in ONE round-trip, in the same two sections and the same\norder the web `/inbox` and `/m/inbox` render. Built from the same\n`Inbox::SubjectGrouping` the web uses, so a notification cannot land in a\ndifferent group on mobile than it does on the desktop.\n\nBoth sections group by **subject** — what an item is ABOUT (\"time-off\nrequests\", \"the ticket I filed\") — rather than by how it was produced.\nSubjects come from the categories the user's own notification-preferences\npage already names, so a group heading always has a preference toggle\nbehind it.\n\n### Needs you\n\nOpen asks waiting on this user: `action_required` and not yet completed.\nThis is the zone whose count the bell badge reports and the only zone a\nuser can empty by doing the work.\n\nOrdered **urgent first, then longest waiting first** (`ordering:\n\"longest_waiting_first\"`). Age alone would bury a brand-new urgent ask\nunder a week-old routine one; priority alone lets an ask rot quietly.\nRender entries in the order received.\n\nEach entry carries the four facts the web row shows:\n\n| field | renders as |\n|---|---|\n| `count` | the count pill |\n| `unread_count` | the \"N new\" pill (omit the pill when 0) |\n| `high_priority` | the **Urgent** pill |\n| `oldest_waiting_label` | \"oldest waiting 4 days\" |\n\n**Adaptive collapse.** A subject only becomes a group once it holds 3+\nitems. Below that its asks arrive as single rows with `group: false`,\nbecause grouping a handful of notifications produces four groups of one —\nstrictly worse than a short list. Read `group` to decide which shape to\ndraw; both carry the same keys, so one list renderer handles both.\n\nFor a group, `title` is the subject label; for a single row it is the\nnotification's own title. `notifications` carries the entry's rows so a\nclient can expand without a second request.\n\n### Activity\n\nEverything that merely happened, **counted and never listed**: the zone\nis unbounded (it is the entire history of things that happened to you)\nand the point of it is that the user does not have to read the pile. Each\nsubsection carries exactly four facts plus its identity — `count`,\n`unread_count`, `newest_at` (when the latest notification in it was\nreceived) and `icon`. There is deliberately no `notifications` array;\nfetch one subsection's rows with `GET /notifications?filter=<key>` (or the\nequivalent `?subject=<key>`) when\nthe user expands it.\n\nOrdered **biggest pile first** (`ordering: \"largest_first\"`).\n\n### Visibility and the empty state\n\nBoth sections show only what the inbox can currently display: archived,\nsnoozed and expired rows are excluded. `empty` is true when both zones\nare empty. `snoozed_count` is reported only when nothing is waiting on\nthe user, and exists to explain a zero — a snoozed ask the bell had\ncounted is invisible here, and without naming it \"You're all caught up\"\nreads as a contradiction.\n\nRelated endpoints: `GET /notifications/open_asks_count` for the bell\nbadge alone, and `GET /notifications?filter=<key>` for one subject's\nrows.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "subject",
            "in": "query",
            "required": false,
            "description": "Narrow BOTH sections to one subject key (a `key` from either section), matching the web `?subject=` filter. Omit it for both zones in full.\n\nSpelling is case- and separator-insensitive over the key, exactly as on `?filter=`. An unrecognised key is a `400 invalid_subject` rather than an empty screen: narrowing both zones to nothing would answer `empty: true` and read as \"you're all caught up\" when the truth is that the filter does not exist. The error message names near-miss keys.\n",
            "schema": {
              "type": "string"
            },
            "example": "leave_notifications"
          }
        ],
        "responses": {
          "200": {
            "description": "The two zones, sectioned and ordered as the web renders them",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "needs_you",
                    "activity",
                    "empty",
                    "snoozed_count"
                  ],
                  "properties": {
                    "needs_you": {
                      "$ref": "#/components/schemas/NotificationsHomeNeedsYou"
                    },
                    "activity": {
                      "$ref": "#/components/schemas/NotificationsHomeActivity"
                    },
                    "empty": {
                      "type": "boolean",
                      "description": "True when both sections are empty.",
                      "example": false
                    },
                    "snoozed_count": {
                      "type": "integer",
                      "description": "Items the user parked, which are hidden from both sections and from the bell. Reported only when nothing is waiting (otherwise 0), because its only job is to explain a zero — and narrowed by `subject` when one was given, so it explains the zero on the screen the caller is looking at rather than naming parked items from other subjects.\n",
                      "example": 0
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_subject` — `subject` names no known subject key. The message names near-miss keys.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/notifications/dismiss_subject": {
      "post": {
        "tags": [
          "Notifications"
        ],
        "summary": "Dismiss a whole Activity subsection (\"Dismiss these\")",
        "description": "Clear one Activity subsection in a single call — the native twin of the\n**Dismiss these** button the web `/inbox` and `/m/inbox` render at the\nfoot of an expanded Activity group. Pass a `key` straight off\n`GET /notifications/home` → `activity.sections[].key` and every\nnotification that section counted is archived.\n\nActivity items are never individually actionable, which is why the zone\noffers no per-row verb to loop instead: the alternative is asking the\nuser to tap twenty times, and twenty round-trips, to reach the same\nstate. This is one request and two queries however large the pile.\n\n### What it takes\n\nExactly the population the section's count described, so the number on\nthe chip is the number of rows that move:\n\n| included | excluded |\n|---|---|\n| Informational rows (things that merely happened) | Open asks — the **Needs you** zone, whatever subject they share |\n| Read rows as well as unread — a read pile is still a pile | Snoozed rows: the user parked them, and they were never counted |\n| | Expired rows, and rows already archived |\n\nIt **archives**, so it is reversible: dismissed rows stay findable under\n`GET /notifications?folder=archived`, and\n`PATCH /notifications/{id}/unarchive` puts one back.\n\nIdempotent — dismissing an already-cleared subject is a `200` with\n`count: 0`, not an error.\n\n### Choosing a subject\n\n`subject` is **required**. Over HTTP an omitted field is far more likely\nto be a client bug than an instruction to clear the whole zone, so\nclearing everything takes the literal word `all`.\n\nValues are the subject keys the notification-category registry derives —\nthe same ones `/notifications/home` emits and `?filter=` accepts\n(`wikis`, `system_announcements`, `kind:dm`, …). Spelling of a KEY is\ncase- and separator-insensitive, so `news_feed_notifications`,\n`news-feed-notifications` and `News Feed Notifications` are one value.\nDisplay **labels** are not accepted — the label of\n`news_feed_notifications` is \"News Feed\", and `subject=News Feed` is a\n`400` whose message names the real key. Send the `key`, not the `label`.\nAn unrecognised key is a `400`, never an empty success: resolving it to\nzero rows would report \"dismissed 0\" for a subject the user can plainly\nsee twelve of.\n\n### Repainting the screen\n\nThe response carries the count and nothing else. Refetch\n`GET /notifications/home` when you want the server's numbers back;\nrecomputing both zones on every dismiss would charge every caller for a\npayload most of them discard, since a client that just cleared a section\nalready knows to drop it. The bell needs no second call either —\n`unread_notification_count` rides along on this response as on every\nother.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "subject",
            "in": "query",
            "required": false,
            "description": "The subject to dismiss. May be sent as a query parameter or in the request body; the body is preferred. See `subject` in the request body schema.\n",
            "schema": {
              "type": "string"
            },
            "example": "system_announcements"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "subject"
                ],
                "properties": {
                  "subject": {
                    "type": "string",
                    "description": "An activity subject key from `GET /notifications/home` (`activity.sections[].key`), or `all` to dismiss the whole Activity zone. Case- and separator-insensitive.\n",
                    "example": "system_announcements"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The subsection was dismissed. `count` is how many notifications were archived, and is 0 when there was nothing left to clear.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "subject",
                    "all",
                    "count"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "subject": {
                      "type": "string",
                      "description": "The canonical subject key that was dismissed, or `all`. Echoed so a client that sent a non-canonical spelling of the key (\"News-Feed-Notifications\", \"ALL\") can match the answer to the section it cleared.\n",
                      "example": "system_announcements"
                    },
                    "all": {
                      "type": "boolean",
                      "description": "True when the whole Activity zone was cleared rather than one subsection. Stated rather than left to be inferred from `subject`, because an undo prompt or an analytics event needs to know which of the two happened.\n",
                      "example": false
                    },
                    "count": {
                      "type": "integer",
                      "description": "Notifications archived by this call.",
                      "example": 12
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`subject_required` when `subject` is missing or blank, or `invalid_subject` when it names no known subject. The message names a value that would have worked.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/notifications/{id}/mark_as_read": {
      "patch": {
        "tags": [
          "Notifications"
        ],
        "summary": "Mark a notification as read",
        "description": "Marks a single notification as read. Sets the `read` flag to `true`\nand records the `read_at` timestamp.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Notification ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Notification marked as read",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "notification": {
                      "$ref": "#/components/schemas/Notification"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Notification not found"
          }
        }
      }
    },
    "/notifications/{id}/mark_as_unread": {
      "patch": {
        "tags": [
          "Notifications"
        ],
        "summary": "Mark a notification as unread",
        "description": "Marks a single notification as unread. Sets the `read` flag to `false`\nand clears the `read_at` timestamp.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Notification ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Notification marked as unread",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "notification": {
                      "$ref": "#/components/schemas/Notification"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Notification not found"
          }
        }
      }
    },
    "/notifications/mark_all_as_read": {
      "patch": {
        "tags": [
          "Notifications"
        ],
        "summary": "Mark all notifications as read",
        "description": "Marks multiple notifications as read in bulk. If `ids` is provided in the\nrequest body, only those specific notifications are marked. Otherwise, all\nunread notifications for the current user are marked as read.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "Optional list of specific notification IDs to mark as read. If omitted, marks all unread notifications.",
                    "example": [
                      1,
                      2,
                      3
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Notifications marked as read",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "count"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "count": {
                      "type": "integer",
                      "description": "Number of notifications marked as read",
                      "example": 5
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    },
    "/notifications/{id}/archive": {
      "patch": {
        "tags": [
          "Notifications"
        ],
        "summary": "Archive a notification",
        "description": "Archives a single notification. Archived notifications are excluded from\nthe default notification list.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Notification ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Notification archived",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "notification": {
                      "$ref": "#/components/schemas/Notification"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Notification not found"
          }
        }
      }
    },
    "/notifications/{id}/unarchive": {
      "patch": {
        "tags": [
          "Notifications"
        ],
        "summary": "Unarchive a notification",
        "description": "Restores a previously archived notification back to the active list.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Notification ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Notification unarchived",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "notification": {
                      "$ref": "#/components/schemas/Notification"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Notification not found"
          }
        }
      }
    },
    "/notifications/archive_all": {
      "patch": {
        "tags": [
          "Notifications"
        ],
        "summary": "Archive multiple notifications",
        "description": "Archives multiple notifications in bulk. If `ids` is provided, only those\nspecific notifications are archived. Otherwise, all notifications for the\ncurrent user are archived.\n\nPass `unarchive: true` to unarchive instead of archive.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "Optional list of specific notification IDs to archive",
                    "example": [
                      1,
                      2,
                      3
                    ]
                  },
                  "unarchive": {
                    "type": "boolean",
                    "description": "If true, unarchives the notifications instead",
                    "default": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Notifications archived/unarchived",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    },
    "/broadcasts": {
      "get": {
        "tags": [
          "Broadcasts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List the caller's received broadcasts (Dashboard inbox)",
        "description": "Returns the SAME broadcasts and ordering the web Broadcast app shows in\nthe Dashboard > \"Broadcasts\" section: the signed-in user's personal\ninbox of **published** broadcasts they received (snapshot membership) or\nauthored — never the full business broadcast list.\n\nOrdering matches the web priority sort: critical + unacknowledged first,\nthen critical + unread, then critical, then unacknowledged, then unread,\nthen read; ties break by `published_at DESC`, then `id DESC`.\n\nSupports the same four Dashboard subfilters via `filter`. Delegates to\n`BroadcastQueries#my_broadcasts` so the API can't drift from the web.\n",
        "parameters": [
          {
            "name": "filter",
            "in": "query",
            "description": "Item subfilter. Default `all`.\n  * `all`         — every received published broadcast\n  * `unread`      — broadcasts the caller has not viewed\n  * `critical`    — `is_critical = true`\n  * `acknowledge` — EVERY acknowledgment-required broadcast\n                    (`require_acknowledgment = true`), regardless of\n                    whether the caller has acknowledged. (This differs\n                    from the web \"Acknowledge\" chip, which shows only\n                    pending acknowledgments.)\n",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "unread",
                "critical",
                "acknowledge"
              ],
              "default": "all"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number (default 1).",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Items per page (default 25, max 100).",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Broadcasts listed (mirrors GET /inspections — collection + meta)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "broadcasts",
                    "pending_approvals",
                    "pending_acknowledgements",
                    "can_manage",
                    "meta"
                  ],
                  "properties": {
                    "broadcasts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BroadcastSummary"
                      }
                    },
                    "pending_acknowledgements": {
                      "type": "array",
                      "maxItems": 25,
                      "description": "Acknowledgment-required broadcasts the caller has NOT\nacknowledged yet ({id, title} only), newest published first,\ncapped at 25 (a preview, not the whole set).\nIndependent of pagination/?filter=. Keyed off the\nnot-acknowledged flag (distinct from meta.segment_counts.\nacknowledge, which is keyed off not-read).\n",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "title": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "pending_approvals": {
                      "type": "array",
                      "description": "Broadcasts awaiting the CALLER's approval (pending Comms Hub\napproval requests whose current step targets the caller's\nrole; admins see all). Same item shape as `broadcasts`.\nIndependent of pagination/?filter=.\n",
                      "items": {
                        "$ref": "#/components/schemas/BroadcastSummary"
                      }
                    },
                    "can_manage": {
                      "type": "boolean",
                      "description": "Root-level capability flag (not per item): whether the caller\ncan manage broadcasts generally — admin/above or the\nbroadcasts manage/edit permission.\n"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/BroadcastListMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "The Broadcasts & Alerts app is not enabled for this business"
          }
        }
      },
      "post": {
        "tags": [
          "Broadcasts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Create and send a broadcast immediately (composers)",
        "description": "Mirrors the web composer's **Send Now** — there is no draft on mobile.\nThe broadcast is created and **published immediately** (status\n`published`, fan-out begins). Requires the broadcasts **create**\npermission (admins/super-admins have it; managers/members when granted).\n\n**Approval:** if an enforced approval workflow governs broadcasts, a\nnon-admin's send is routed through approval instead of publishing — the\nbroadcast is submitted for review and the response returns\n`status: \"pending_approval\"`. Admins override approval and publish\ndirectly (same as web).\n\nAt least one recipient target MUST be supplied (`audience_id`,\n`notification_recipient_group_ids`, `extra_user_ids`, or\n`audience_criteria`); a request with no target — or one that resolves to\nnobody in this business — returns `422 no_recipients` and nothing is\npersisted (no lingering draft).\n\n**Send later:** pass `scheduled_at` and the broadcast is left in status\n`scheduled` for the tick job instead of fanning out now — the response\ncarries `status: \"scheduled\"`. The approval and moderation gates run\nFIRST, so a broadcast that would be held for review cannot slip out on a\ntimer. A past or unparseable time is `422 invalid_scheduled_at`, never a\nsilent immediate send.\n\n**Title is optional.** The unified Communications composer has no Title\nfield, so a blank or absent `title` is derived from the first line of\n`description` (capped at a headline length) rather than rejected. A\nblank `description` is still a validation error.\n\n**Break-room screens:** pass `publish_to_signage` (optionally narrowed\nby `signage_location_ids`) to also put the broadcast into the Digital\nSignage rotation. Screens PULL — nothing is delivered by this request,\nand each screen's own content rules still decide whether it shows. If\nthe tenant has no active screen the broadcast still sends on its other\nchannels and the 201 carries a `warnings` entry saying nothing was\nqueued, rather than failing the send.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "broadcast"
                ],
                "properties": {
                  "broadcast": {
                    "type": "object",
                    "required": [
                      "description"
                    ],
                    "properties": {
                      "title": {
                        "type": "string",
                        "description": "Optional. The subject line every channel renders (push,\nemail, SMS) and the headline of the broadcast's News\nFeed record. When blank or absent it is DERIVED from the\nfirst line of `description`, so a composer with no Title\nfield never has to send the message twice.\n"
                      },
                      "description": {
                        "type": "string",
                        "description": "The message body. Required."
                      },
                      "is_critical": {
                        "type": "boolean"
                      },
                      "require_acknowledgment": {
                        "type": "boolean"
                      },
                      "allow_comments": {
                        "type": "boolean"
                      },
                      "allow_reactions": {
                        "type": "boolean"
                      },
                      "auto_widen_channels": {
                        "type": "boolean",
                        "description": "Automatically widen the delivery channel while an\nacknowledgement is still outstanding: each reminder adds\nthe next channel (in-app + email, then +SMS, then\n+voice). Only meaningful together with\n`require_acknowledgment` — the reminder job that drives\nit runs for required-reading broadcasts only. Honors\nquiet hours, channel rules and notification preferences.\nOmit the key to leave it at the tenant default (off).\n"
                      },
                      "delivery_mode": {
                        "type": "string",
                        "enum": [
                          "immediate",
                          "on_shift_only",
                          "next_shift_start"
                        ],
                        "default": "immediate",
                        "description": "The shift-aware delivery window:\n\n  * `immediate` — send to everyone now (default).\n  * `on_shift_only` — deliver only to recipients who are\n    currently on shift; off-shift recipients are\n    suppressed.\n  * `next_shift_start` — hold off-shift recipients and\n    deliver at the start of their next shift.\n\nCritical alerts ignore this and always break through\nimmediately. An unrecognized value degrades to\n`immediate` rather than holding the send back.\n"
                      },
                      "scheduled_at": {
                        "type": "string",
                        "format": "date-time",
                        "description": "Send the broadcast at a future time instead of now. Parsed\nin the CALLER's time zone, so a bare\n`\"2026-08-12 09:00\"` means 9am where the author is. The\nrecord is left in status `scheduled` and published by\n`ScheduledBroadcastPublishJob`. Must be in the future —\na past or unparseable value returns\n`422 invalid_scheduled_at`.\n"
                      },
                      "channels": {
                        "type": "array",
                        "description": "Delivery channels to ENABLE (email / sms / voice). Any not\nlisted are disabled. `in_app` always delivers and `push`\nstays on regardless. Omit the key entirely to leave all\nchannels at their default (on).\n",
                        "items": {
                          "type": "string",
                          "enum": [
                            "email",
                            "sms",
                            "voice"
                          ]
                        }
                      },
                      "publish_to_signage": {
                        "type": "boolean",
                        "description": "The composer's **Break-room screens** channel — mark this\nbroadcast for the Digital Signage rotation, the channel\nthat reaches frontline staff with no work phone and no\nwork email.\n\nIt is a REQUEST, not a delivery. Screens PULL: each one\nshows the broadcast on its next refresh, and only where\nits signage admin has \"Communications posts\" switched on.\nOnly PUBLISHED broadcasts are picked up, and only for 14\ndays, so a draft or a held send never reaches a wall.\n\nScreens are token-authenticated PUBLIC web pages —\nanyone standing in the room can read them. Only set this\nfor something you would put on a wall.\n\nDeliberately separate from `channels`: that array is the\nper-USER delivery map, and a screen is a place, not a\nrecipient.\n\nOmit the key to leave the selection unchanged. On PATCH,\nsend `false` to take a broadcast back off the screens.\n"
                      },
                      "signage_location_ids": {
                        "type": "array",
                        "description": "Narrow `publish_to_signage` to specific sites. Omit or\nsend an empty array to show on every screen. Ids outside\nthe caller's business are dropped, and the key is ignored\nentirely unless `publish_to_signage` is on.\n",
                        "items": {
                          "type": "integer"
                        }
                      },
                      "audience_id": {
                        "type": "integer",
                        "nullable": true,
                        "description": "A saved CommsHub audience to send to."
                      },
                      "extra_user_ids": {
                        "type": "array",
                        "description": "Specific user ids to send to.",
                        "items": {
                          "type": "integer"
                        }
                      },
                      "audience_criteria": {
                        "type": "array",
                        "description": "Attribute-based audience filters, each a typed hash — e.g.\n`{ \"type\": \"role\", \"roles\": [\"member\"] }`,\n`{ \"type\": \"job_title\", \"titles\": [\"Area Manager\"] }`,\n`{ \"type\": \"department\", \"ids\": [1,2] }`,\n`{ \"type\": \"location\", \"ids\": [3] }`.\n",
                        "items": {
                          "type": "object",
                          "additionalProperties": true
                        }
                      },
                      "media_signed_ids": {
                        "type": "array",
                        "description": "Attachments. Pre-upload each file via\n`POST /rails/active_storage/direct_uploads` (standard\nActiveStorage direct upload) and pass the resulting blob\n`signed_id`s here. They are attached to the broadcast's\n`media_files` (Drive) — the same attachments the web\ncomposer and the show API expose. Validated server-side\nagainst the broadcast media rules (image/pdf/video, per-file\nsize cap, max 10 files); any invalid reference fails the\nwhole create with 422 and nothing is persisted.\n",
                        "items": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "notification_recipient_group_ids": {
                    "type": "array",
                    "description": "Notification recipient group ids to send to (top-level, not under `broadcast`).",
                    "items": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Broadcast created. Normally **published** immediately\n(`broadcast.status: \"published\"`). Two other outcomes are also 201:\n\n  * **submitted for approval** — an enforced approval workflow\n    governs broadcasts and the caller is NOT an admin (admins\n    override), or the request set `submit_for_approval`. The\n    response carries `status: \"pending_approval\"`; an approver sends\n    it later via `POST /approvals/{id}/approve`.\n\n    `broadcast.status` is `\"draft\"` for a send-now composition, and\n    `\"scheduled\"` when the request also carried `scheduled_at` — the\n    author's send time is KEPT through the review so the approver\n    can see when it goes out and it still sends at that time once\n    approved. It cannot slip out early: the tick job refuses any\n    broadcast whose approval request is still pending.\n  * **scheduled** — `scheduled_at` was supplied. The response carries\n    `status: \"scheduled\"` and `broadcast.status: \"scheduled\"`; the\n    tick job publishes it at the requested time.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "broadcast"
                  ],
                  "properties": {
                    "broadcast": {
                      "$ref": "#/components/schemas/BroadcastDetail"
                    },
                    "channels": {
                      "type": "array",
                      "description": "The channels the broadcast will fan out on (in_app always; email/sms/voice/push unless toggled off).",
                      "items": {
                        "type": "string"
                      }
                    },
                    "attachments": {
                      "type": "array",
                      "description": "The attachments that were attached to the broadcast (from media_signed_ids).",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "filename": {
                            "type": "string"
                          },
                          "content_type": {
                            "type": "string"
                          },
                          "byte_size": {
                            "type": "integer"
                          },
                          "url": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending_approval"
                      ],
                      "description": "Present only when the send was routed through approval instead of publishing."
                    },
                    "message": {
                      "type": "string"
                    },
                    "warnings": {
                      "type": "array",
                      "description": "Non-fatal notices about parts of the request that could not\nbe honoured — the broadcast still sent. Present only when\nnon-empty. Today this carries the outcome of a\n`publish_to_signage` request: the confirmation naming how\nmany screens it was queued for, or the reason nothing was\n(no active screen registered, or none at the sites picked).\n",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "No permission to create broadcasts, or app not enabled"
          },
          "422": {
            "description": "Validation failed (`validation_failed`); no recipient target supplied\nor it resolved to nobody (`no_recipients`); or approval is required but\nthe request could not be submitted (`approval_required`).\n"
          }
        }
      }
    },
    "/broadcasts/templates": {
      "get": {
        "tags": [
          "Broadcasts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List broadcast templates for the composer's \"Start from a template\" picker",
        "description": "Powers the broadcast composer's \"Start from a template\" picker (opened\nfrom the broadcast list `+`). Returns the two groups the web Broadcast app\noffers:\n  * `your_templates`    — this business's saved broadcast templates (ordered)\n  * `gallery_templates` — the platform-curated \"Browse Library\" templates\n                          (system templates shared across businesses), so an\n                          author can start from a pre-built composition.\nEach entry carries the fields the new-broadcast form pre-fills from a\ntemplate (title ← `name`, description ← `body`, the critical / ack /\ncomment / reaction flags, and the saved audience + recipient groups).\nRequires the broadcasts **create** permission — the same gate as\n`POST /broadcasts` (admins/super-admins always; managers/members when\ngranted). Anyone who can create a broadcast can fetch its templates.\n",
        "responses": {
          "200": {
            "description": "Templates listed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "your_templates",
                    "gallery_templates",
                    "meta"
                  ],
                  "properties": {
                    "your_templates": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "name",
                          "body",
                          "system_template"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string",
                            "description": "Template name — pre-fills the new broadcast's title."
                          },
                          "description": {
                            "type": "string",
                            "nullable": true
                          },
                          "body": {
                            "type": "string",
                            "description": "Template body — pre-fills the new broadcast's description/body."
                          },
                          "is_critical": {
                            "type": "boolean"
                          },
                          "require_acknowledgment": {
                            "type": "boolean"
                          },
                          "allow_comments": {
                            "type": "boolean"
                          },
                          "allow_reactions": {
                            "type": "boolean"
                          },
                          "audience_id": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Saved CommsHub audience to pre-fill the recipient picker, if any."
                          },
                          "notification_recipient_group_ids": {
                            "type": "array",
                            "items": {
                              "type": "integer"
                            },
                            "description": "Saved recipient group ids to pre-fill the recipient picker."
                          },
                          "system_template": {
                            "type": "boolean",
                            "description": "True for a platform-curated gallery template (business_id is null)."
                          },
                          "metadata": {
                            "type": "object",
                            "description": "Saved composition extras the composer pre-fills from —\ne.g. `channel_rules`, `audience_criteria`, `extra_user_ids`.\n",
                            "additionalProperties": true
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "gallery_templates": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "name",
                          "body",
                          "system_template"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string",
                            "description": "Template name — pre-fills the new broadcast's title."
                          },
                          "description": {
                            "type": "string",
                            "nullable": true
                          },
                          "body": {
                            "type": "string",
                            "description": "Template body — pre-fills the new broadcast's description/body."
                          },
                          "is_critical": {
                            "type": "boolean"
                          },
                          "require_acknowledgment": {
                            "type": "boolean"
                          },
                          "allow_comments": {
                            "type": "boolean"
                          },
                          "allow_reactions": {
                            "type": "boolean"
                          },
                          "audience_id": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Saved CommsHub audience to pre-fill the recipient picker, if any."
                          },
                          "notification_recipient_group_ids": {
                            "type": "array",
                            "items": {
                              "type": "integer"
                            },
                            "description": "Saved recipient group ids to pre-fill the recipient picker."
                          },
                          "system_template": {
                            "type": "boolean",
                            "description": "True for a platform-curated gallery template (business_id is null)."
                          },
                          "metadata": {
                            "type": "object",
                            "description": "Saved composition extras the composer pre-fills from —\ne.g. `channel_rules`, `audience_criteria`, `extra_user_ids`.\n",
                            "additionalProperties": true
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "your_templates_count",
                        "gallery_templates_count"
                      ],
                      "properties": {
                        "your_templates_count": {
                          "type": "integer"
                        },
                        "gallery_templates_count": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "No permission to create broadcasts, or the app is not enabled"
          }
        }
      }
    },
    "/broadcasts/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Broadcast ID",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Broadcasts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get a broadcast",
        "description": "Returns the broadcast detail plus `meta.segment_counts` — the same\n{ all, critical, acknowledge } NOT-READ inbox counts the list endpoint\nexposes, so the detail screen can keep the inbox badges current without a\nseparate list request. The counts reflect the inbox state AFTER this\nbroadcast is marked read by the fetch.\n",
        "responses": {
          "200": {
            "description": "Broadcast found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "broadcast",
                    "meta"
                  ],
                  "properties": {
                    "broadcast": {
                      "$ref": "#/components/schemas/BroadcastDetail"
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "segment_counts"
                      ],
                      "properties": {
                        "segment_counts": {
                          "type": "object",
                          "description": "NOT-READ inbox counts for the caller (mirrors the list\nendpoint). `all` is the unread total — no separate\n`unread` key.\n",
                          "required": [
                            "all",
                            "critical",
                            "acknowledge"
                          ],
                          "properties": {
                            "all": {
                              "type": "integer"
                            },
                            "critical": {
                              "type": "integer"
                            },
                            "acknowledge": {
                              "type": "integer"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Not permitted to view this broadcast (not a recipient, author, or manager/admin)"
          },
          "404": {
            "description": "Broadcast not found"
          }
        }
      },
      "patch": {
        "tags": [
          "Broadcasts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Update a draft broadcast (admin only)",
        "description": "Only broadcasts still in `draft` status can be edited via the API.\n\nA PARTIAL edit: send only the attributes you are changing. Omitted keys\nare left alone — including `title`, which is no longer re-derived from a\n`description` you did send.\n\n`publish_to_signage` follows PATCH semantics: omit it to leave the\nbreak-room screen selection alone, send `false` to take the broadcast\noff the screens. A selection that could not be honoured comes back in a\n`warnings` array rather than failing the edit.\n\nRecipients (`extra_user_ids`) and the `channels` mix are set at create\ntime and cannot be edited; sending either changes nothing and is named\nin `warnings`.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "broadcast"
                ],
                "properties": {
                  "broadcast": {
                    "$ref": "#/components/schemas/BroadcastInput"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Broadcast updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "broadcast"
                  ],
                  "properties": {
                    "broadcast": {
                      "$ref": "#/components/schemas/BroadcastDetail"
                    },
                    "warnings": {
                      "type": "array",
                      "description": "Non-fatal notices — present only when non-empty. Carries the outcome of a `publish_to_signage` request (the confirmation naming how many screens it was queued for, or the reason nothing was), plus a note naming any create-only key (`extra_user_ids`, `channels`) that was sent and therefore ignored.",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Admin access required, or broadcast is not a draft"
          },
          "404": {
            "description": "Broadcast not found"
          },
          "422": {
            "description": "Validation failed"
          }
        }
      }
    },
    "/broadcasts/{id}/publish": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Broadcast ID",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Broadcasts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Publish a draft broadcast (admin only)",
        "description": "Runs the model's `publish!` so the API matches the web path: recipient\nsnapshot, variant assignment, and the notification fan-out all run.\n",
        "responses": {
          "200": {
            "description": "Broadcast published",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "broadcast"
                  ],
                  "properties": {
                    "broadcast": {
                      "$ref": "#/components/schemas/BroadcastDetail"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Admin access required, or broadcast is already published"
          },
          "404": {
            "description": "Broadcast not found"
          },
          "422": {
            "description": "Could not publish broadcast"
          }
        }
      }
    },
    "/broadcasts/{id}/acknowledge": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Broadcast ID",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Broadcasts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Acknowledge a broadcast (recipient)",
        "description": "Records the caller's acknowledgment of a published broadcast that requires it.",
        "responses": {
          "200": {
            "description": "Acknowledged",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "acknowledged",
                    "broadcast_id"
                  ],
                  "properties": {
                    "acknowledged": {
                      "type": "boolean"
                    },
                    "broadcast_id": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "404": {
            "description": "Broadcast not found"
          },
          "422": {
            "description": "Broadcast is not published or does not require acknowledgment"
          }
        }
      }
    },
    "/broadcasts/{id}/viewers": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Broadcast ID",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Broadcasts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List recipients who have viewed the broadcast (read receipts)",
        "description": "Read-receipt roster powering the \"Viewed by N of M\" flyout on the\nBroadcast detail screen. Returns the recipients who have **viewed** the\nbroadcast, ordered most-recently-viewed first and paginated, plus the\ntotal-recipients and unique-view counts in `meta`.\n\nAccessible to anyone who can see the broadcast itself: a **recipient**\n(in the recipient list), the **author**, or a **manager/admin**. Drafts\nhave no recipient list, so they are limited to the author and\nmanagers/admins. Anyone else gets `403`.\n",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number (default 1).",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Items per page (default 25, max 100).",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Viewers listed (most-recently-viewed first)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "viewers",
                    "meta"
                  ],
                  "properties": {
                    "viewers": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "viewed_at"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string",
                            "nullable": true
                          },
                          "avatar_url": {
                            "type": "string",
                            "nullable": true
                          },
                          "title": {
                            "type": "string",
                            "nullable": true,
                            "description": "Viewer's job title (row subtitle)."
                          },
                          "location": {
                            "type": "string",
                            "nullable": true,
                            "description": "Viewer's office location (row subtitle)."
                          },
                          "viewed_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "total_recipients_count",
                        "unique_view_count",
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ],
                      "properties": {
                        "total_recipients_count": {
                          "type": "integer",
                          "description": "Total recipients the broadcast reached (snapshot size — the \"M\")."
                        },
                        "unique_view_count": {
                          "type": "integer",
                          "description": "Distinct recipients who have viewed it (the \"N\")."
                        },
                        "total_count": {
                          "type": "integer"
                        },
                        "total_pages": {
                          "type": "integer"
                        },
                        "current_page": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Caller is not a recipient, the author, or a manager/admin for this broadcast"
          },
          "404": {
            "description": "Broadcast not found"
          }
        }
      }
    },
    "/broadcasts/{id}/acknowledgements": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Broadcast ID",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Broadcasts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List recipients by acknowledgement state (Acknowledgement tracking)",
        "description": "Acknowledgement-tracking roster powering the \"Acknowledgement tracking\"\nflyout (Acknowledged / Pending tabs) on the Broadcast detail screen.\nReturns the broadcast's recipients filtered by whether they have\nacknowledged it, in the same order as the web \"View Recipients\" page\n(user id), paginated, plus the total / acknowledged / not-acknowledged\ncounts in `meta`.\n\nOnly meaningful for `require_acknowledgment` broadcasts (others return\n`422`). Restricted to callers who can see \"View stats\" — a **published**\nbroadcast they can **manage** (admin/manager, or the author when a\nmanager+). Anyone else gets `403`.\n",
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "description": "Which roster to return. Default `acked`.\n  * `acked`     — recipients who HAVE acknowledged (each row carries\n                  the `acknowledged_at` time).\n  * `not_acked` — recipients who have NOT acknowledged\n                  (`acknowledged_at` is null).\n",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "acked",
                "not_acked"
              ],
              "default": "acked"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number (default 1).",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Items per page (default 20, max 100).",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Recipients listed (filtered by acknowledgement state, user-id order)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "recipients",
                    "meta"
                  ],
                  "properties": {
                    "recipients": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string",
                            "nullable": true
                          },
                          "avatar_url": {
                            "type": "string",
                            "nullable": true
                          },
                          "title": {
                            "type": "string",
                            "nullable": true,
                            "description": "Recipient's job title (row subtitle)."
                          },
                          "acknowledged_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "description": "When this recipient acknowledged (null on the not_acked roster)."
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "type",
                        "total_recipients_count",
                        "acked_count",
                        "not_acked_count",
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ],
                      "properties": {
                        "type": {
                          "type": "string",
                          "description": "The resolved roster type (acked | not_acked)."
                        },
                        "total_recipients_count": {
                          "type": "integer",
                          "description": "Total recipients the broadcast reached."
                        },
                        "acked_count": {
                          "type": "integer",
                          "description": "Recipients who have acknowledged."
                        },
                        "not_acked_count": {
                          "type": "integer",
                          "description": "Recipients who have not acknowledged."
                        },
                        "total_count": {
                          "type": "integer",
                          "description": "Items matching the active type (this list's size)."
                        },
                        "total_pages": {
                          "type": "integer"
                        },
                        "current_page": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Caller cannot see \"View stats\" (not published, or not a manager/admin/author)"
          },
          "404": {
            "description": "Broadcast not found"
          },
          "422": {
            "description": "Broadcast does not require acknowledgment"
          }
        }
      }
    },
    "/broadcasts/{id}/comments": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Broadcast ID",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Broadcasts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List a broadcast's comments (paginated)",
        "description": "Full, paginated comment feed for the broadcast — the same top-level\ncomments the detail screen renders. Each item has the IDENTICAL shape to\nthe `recent_comments` items in GET /broadcasts/{id} ({ id, body,\ncreated_at, author, attachments }); the only difference is this returns\nthe WHOLE feed page-by-page instead of just the latest 5. Top-level\ncomments only (replies excluded), newest first (`created_at DESC`).\n\nAccessible to anyone who can see the broadcast itself: a **recipient**,\nthe **author**, or a **manager/admin** (same gate as show). Anyone else\ngets `403`.\n",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number (default 1).",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Items per page (default 25, max 100).",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Comments listed (newest first)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "comments",
                    "meta"
                  ],
                  "properties": {
                    "comments": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BroadcastComment"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ],
                      "properties": {
                        "total_count": {
                          "type": "integer"
                        },
                        "total_pages": {
                          "type": "integer"
                        },
                        "current_page": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Caller is not a recipient, the author, or a manager/admin for this broadcast"
          },
          "404": {
            "description": "Broadcast not found"
          }
        }
      },
      "post": {
        "tags": [
          "Broadcasts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Add a comment to a broadcast",
        "description": "Creates a comment, mirroring the web BroadcastCommentsController#create\n(including content moderation — a held comment is hidden until a moderator\napproves it and the response carries `held: true`).\n\nAccessible to anyone who can see the broadcast (recipient / author /\nmanager / admin) when commenting is enabled on it (`allow_comments`).\nReturns the created comment in the canonical shape, including `can_edit`\n/ `can_delete` for the caller.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "comment"
                ],
                "properties": {
                  "comment": {
                    "type": "object",
                    "required": [
                      "content"
                    ],
                    "properties": {
                      "content": {
                        "type": "string",
                        "description": "The comment body."
                      },
                      "parent_id": {
                        "type": "integer",
                        "nullable": true,
                        "description": "Set to reply to an existing comment; omit/null for a top-level comment."
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Comment created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BroadcastCommentWriteResult"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Caller cannot view this broadcast, or commenting is disabled (`comments_disabled`)"
          },
          "404": {
            "description": "Broadcast not found"
          },
          "422": {
            "description": "Validation failed (e.g. blank content)"
          }
        }
      }
    },
    "/broadcasts/{id}/comments/{comment_id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Broadcast ID",
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "comment_id",
          "in": "path",
          "required": true,
          "description": "Comment ID",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "patch": {
        "tags": [
          "Broadcasts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Edit a comment (author only)",
        "description": "Updates a comment's content. Permission matches the `can_edit` flag: the\ncomment's **author only** (an admin editing someone else's comment gets\n`403`). Re-runs content moderation like the web edit.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "comment"
                ],
                "properties": {
                  "comment": {
                    "type": "object",
                    "required": [
                      "content"
                    ],
                    "properties": {
                      "content": {
                        "type": "string",
                        "description": "The comment body."
                      },
                      "parent_id": {
                        "type": "integer",
                        "nullable": true,
                        "description": "Set to reply to an existing comment; omit/null for a top-level comment."
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Comment updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BroadcastCommentWriteResult"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Caller is not the comment's author"
          },
          "404": {
            "description": "Broadcast or comment not found"
          },
          "422": {
            "description": "Validation failed (e.g. blank content)"
          }
        }
      },
      "delete": {
        "tags": [
          "Broadcasts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Delete a comment (author or admin/above)",
        "description": "Soft-deletes a comment. Permission matches the `can_delete` flag and the\nweb delete visibility: the comment's **author OR an admin/above** member.\n",
        "responses": {
          "200": {
            "description": "Comment deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "comment_id"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    },
                    "comment_id": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Caller is neither the comment's author nor an admin/above"
          },
          "404": {
            "description": "Broadcast or comment not found"
          }
        }
      }
    },
    "/broadcasts/{id}/resend": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Broadcast ID",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Broadcasts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Resend a broadcast to pending recipients (\"Resend Now\")",
        "description": "Like the web **Resend Now** control on the View-stats page\n(`BroadcastsController#resend_to_pending`). Re-delivers the broadcast to\nevery recipient who has **not acknowledged** yet, reusing the model's\nper-channel delivery (so delivery receipts and each user's notification\npreferences are honored).\n\nThe resend goes over the channels the broadcast was **configured with at\ncreation** (the author's per-broadcast channel toggles), NOT channels\nsupplied in the request — there is no request body, and any `channels`\nparam is ignored. The channels actually used are echoed back in the\nresponse `channels` array.\n\nAuthorization mirrors the web: the caller must be able to **manage** the\nbroadcast — an admin (or above), a holder of the broadcasts `manage`/`edit`\npermission, or the author when a manager+. Otherwise `403`.\n\nOnly valid for a **published**, **acknowledgment-required** broadcast.\n",
        "responses": {
          "200": {
            "description": "Resent to pending recipients",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "broadcast_id",
                    "channels",
                    "resent_count",
                    "message"
                  ],
                  "properties": {
                    "broadcast_id": {
                      "type": "integer"
                    },
                    "channels": {
                      "type": "array",
                      "description": "The creation-time channels the broadcast was resent over (subset of `email` / `sms` / `voice`).",
                      "items": {
                        "type": "string"
                      }
                    },
                    "resent_count": {
                      "type": "integer",
                      "description": "Number of pending (not-yet-acknowledged) recipients the broadcast was resent to."
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Caller cannot manage this broadcast"
          },
          "404": {
            "description": "Broadcast not found"
          },
          "422": {
            "description": "Not a published, acknowledgment-required broadcast, or no resendable channel was enabled at creation"
          }
        }
      }
    },
    "/broadcasts/{id}/remind": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Broadcast ID",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Broadcasts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Send an acknowledgment reminder to a single recipient",
        "description": "Sends an acknowledgment reminder to ONE recipient — the same reminder the\nweb sends automatically (`BroadcastAcknowledgmentReminderJob#send_reminder`):\nan in-app Notification plus the reminder email, bumping `reminder_count`\nand `last_reminder_at` on the recipient's status row.\n\nAuthorization mirrors the web reminder/resend controls: the caller must be\nable to **manage** the broadcast (admin/above, broadcasts `manage`/`edit`\npermission, or the author when a manager+). Otherwise `403`.\n\nOnly valid for a **published**, **acknowledgment-required** broadcast and a\nrecipient who has **not acknowledged** yet.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "user_id"
                ],
                "properties": {
                  "user_id": {
                    "type": "integer",
                    "description": "The recipient to remind."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reminder sent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "broadcast_id",
                    "user_id",
                    "reminder_count",
                    "message"
                  ],
                  "properties": {
                    "broadcast_id": {
                      "type": "integer"
                    },
                    "user_id": {
                      "type": "integer"
                    },
                    "reminder_count": {
                      "type": "integer",
                      "description": "The recipient's cumulative reminder count after this send."
                    },
                    "last_reminder_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Caller cannot manage this broadcast"
          },
          "404": {
            "description": "Broadcast not found"
          },
          "422": {
            "description": "Not a published / acknowledgment-required broadcast (`cannot_remind`),\n`user_id` missing or not in the business (`user_not_found`), the user is\nnot a recipient (`not_a_recipient`), or the user already acknowledged\n(`already_acknowledged`).\n"
          }
        }
      }
    },
    "/broadcasts/{id}/archive": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Broadcast ID",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Broadcasts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Archive a published broadcast",
        "description": "Archives a **published** broadcast (status → `archived`), mirroring the\nweb \"Archive\" action.\n\nAuthorization mirrors the web archive: the caller must be able to\n**manage** the broadcast (admin/above, broadcasts `manage`/`edit`\npermission, or the author when a manager+). Otherwise `403`.\n\nOnly **published** broadcasts can be archived — drafts and scheduled\nbroadcasts return `422` (they are deleted, not archived).\n",
        "responses": {
          "200": {
            "description": "Broadcast archived (returns the updated broadcast)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "broadcast"
                  ],
                  "properties": {
                    "broadcast": {
                      "$ref": "#/components/schemas/BroadcastDetail"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Caller cannot manage this broadcast"
          },
          "404": {
            "description": "Broadcast not found"
          },
          "422": {
            "description": "Broadcast is not published (`not_published`), or archiving failed (`archive_failed`)"
          }
        }
      }
    },
    "/broadcasts/{broadcast_id}/reactions/toggle": {
      "parameters": [
        {
          "name": "broadcast_id",
          "in": "path",
          "required": true,
          "description": "Broadcast ID",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Broadcasts"
        ],
        "summary": "Toggle (add/remove) an emoji reaction on a broadcast",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "description": "Adds or removes the caller's emoji reaction, mirroring the web\n`BroadcastReactionsController#toggle` (a single TOGGLE per emoji via\n`Platform::Reactable#toggle_reaction`): re-sending the same emoji removes\nthe caller's reaction, otherwise it adds one. `reacted` in the response is\n`true` when the reaction is now ON (added), `false` when removed.\n\nAccessible to anyone who can see the broadcast (recipient / author /\nmanager / admin) when reactions are enabled on it (`allow_reactions`).\nThe emoji must be in the broadcast's allowed set\n(`👍 ❤️ 😄 😢 😮 🎉 👏 ✅`).\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emoji"
                ],
                "properties": {
                  "emoji": {
                    "type": "string",
                    "description": "The emoji to toggle. Must be in the broadcast's allowed reaction set."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reaction toggled",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "broadcast_id",
                    "emoji",
                    "reacted",
                    "reactions",
                    "my_reactions"
                  ],
                  "properties": {
                    "broadcast_id": {
                      "type": "integer"
                    },
                    "emoji": {
                      "type": "string"
                    },
                    "reacted": {
                      "type": "boolean",
                      "description": "True when the reaction is now ON (added); false when it was removed."
                    },
                    "reactions": {
                      "type": "array",
                      "description": "Updated reaction tallies for the broadcast.",
                      "items": {
                        "type": "object",
                        "required": [
                          "emoji",
                          "count"
                        ],
                        "properties": {
                          "emoji": {
                            "type": "string"
                          },
                          "count": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "my_reactions": {
                      "type": "array",
                      "description": "The caller's current emoji reactions on this broadcast.",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Caller cannot view this broadcast, or reactions are disabled (`reactions_disabled`)"
          },
          "404": {
            "description": "Broadcast not found"
          },
          "422": {
            "description": "Emoji not in the broadcast's allowed reaction set (`invalid_emoji`)"
          }
        }
      }
    },
    "/approvals/{id}/approve": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Comms Hub approval request id (from a source's `approval.request_id`).",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Approvals"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Approve a pending approval request (broadcast or alert)",
        "description": "Records the caller's **approve** decision on a pending Comms Hub approval\nrequest — for a broadcast OR an alert — mirroring\n`Apps::CommsHub::ApprovalsController#approve`. Appends an entry to the\nrequest's audit trail and advances the workflow: when the approved step\nis the last one the status becomes `approved`; otherwise it moves to the\nnext step (still `pending`).\n",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "notes": {
                    "type": "string",
                    "description": "Optional reviewer note, stored on the audit entry."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Decision recorded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApprovalDecision"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Caller cannot act on the current step (`forbidden`), or app not enabled (`app_disabled`)"
          },
          "404": {
            "description": "No pending approval request with that id for this business (`not_found`)"
          },
          "422": {
            "description": "Decision could not be recorded (`approval_decision_failed`)"
          }
        }
      }
    },
    "/approvals/{id}/reject": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Comms Hub approval request id (from a source's `approval.request_id`).",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Approvals"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Reject a pending approval request (broadcast or alert)",
        "description": "Records the caller's **reject** decision on a pending Comms Hub approval\nrequest — for a broadcast OR an alert — mirroring\n`Apps::CommsHub::ApprovalsController#reject`. Appends an entry to the\naudit trail and closes the request (status → `rejected`).\n",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "notes": {
                    "type": "string",
                    "description": "Optional reviewer note, stored on the audit entry."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Decision recorded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApprovalDecision"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Caller cannot act on the current step (`forbidden`), or app not enabled (`app_disabled`)"
          },
          "404": {
            "description": "No pending approval request with that id for this business (`not_found`)"
          },
          "422": {
            "description": "Decision could not be recorded (`approval_decision_failed`)"
          }
        }
      }
    },
    "/audience_options": {
      "get": {
        "tags": [
          "Audience Options"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List audience-picker options (departments / locations / job titles / roles / groups)",
        "description": "Powers \"Add to audience\" in the mobile broadcast and alert composers. The\n`type` param selects which attribute to list; each item is\n`{ id, name, user_count }`:\n  * `departments` / `locations` / `groups` → integer `id`\n  * `roles` (admin / manager / member) / `job_titles` → string `id`\n    (the role key / the title text)\n\n`user_count` is the number of ACTIVE business users the entity resolves\nto, computed with the same logic as the audience resolver so the picker\ncount matches the eventual reach. For parametric recipient groups whose\nmembership varies per send, `user_count` is `null`.\n\nSupports `q` (case-insensitive name/title search) and pagination. Mirrors\nthe web BroadcastRecipientParametersController sources (e.g. roles are the\ncanonical admin/manager/member keys — super_admin is excluded).\n",
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "required": true,
            "description": "Which attribute list to return.",
            "schema": {
              "type": "string",
              "enum": [
                "departments",
                "locations",
                "job_titles",
                "roles",
                "groups"
              ]
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Case-insensitive search over the entity name / title.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number (default 1).",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Items per page (default 25, max 100).",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Options listed for the requested type",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "meta"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "name"
                        ],
                        "properties": {
                          "id": {
                            "description": "Integer id (departments/locations/groups) or string key (roles/job_titles).",
                            "oneOf": [
                              {
                                "type": "integer"
                              },
                              {
                                "type": "string"
                              }
                            ]
                          },
                          "name": {
                            "type": "string"
                          },
                          "user_count": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Active users the entity resolves to; null for parametric groups (membership varies)."
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "type",
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ],
                      "properties": {
                        "type": {
                          "type": "string",
                          "enum": [
                            "departments",
                            "locations",
                            "job_titles",
                            "roles",
                            "groups"
                          ]
                        },
                        "total_count": {
                          "type": "integer"
                        },
                        "total_pages": {
                          "type": "integer"
                        },
                        "current_page": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Caller can't compose (not a manager/admin or broadcasts creator), or the app is not enabled"
          },
          "422": {
            "description": "Missing or unknown `type` (`invalid_type`)"
          }
        }
      }
    },
    "/frontline_execution/my_day": {
      "get": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "The worker's filtered, paginated My Day list",
        "description": "Returns one page of the logged-in worker's My Day list for the selected\nfilter, plus the exact count of all four filters on every call so a tab\nbar renders from a single request.\n\nThis is the same ranked list the web My Day shows, so `assigned` includes\nboth the obligations the worker holds AND the must-confirm acknowledgement\n\"posts\" they owe (a post has no single assignee — everyone in its audience\nmust confirm it).\n\n**`filter`** selects one tab (all four are slices of that one list):\n\n| filter     | contains                                                        |\n|------------|-----------------------------------------------------------------|\n| `assigned` | the worker's whole My Day — held obligations + must-confirm posts (default) |\n| `overdue`  | those that are past due and still open                          |\n| `critical` | those whose campaign is marked critical (posts included)        |\n| `pool`     | the claim pool — unclaimed, role-matched work at the worker's sites |\n\n**`counts`** carries all four badges on every response (0 included), each\nthe exact size of that filter's list — `overdue` and `critical` are\nsubsets of `assigned`, so a row can count under more than one.\n\nBecause the list includes posts (resolved in Ruby, not SQL), it is ranked\nand paged in memory over the ranker's bounded output: a page reaches at\nmost the ranker's per-lane cap, exactly as the web list is capped.\n\nRows are ordered by the same rank the worker sees on the web (overdue →\nHQ priority → soonest due → id), a total order so paging is stable across\nrequests. `meta` is the pagination envelope for the SELECTED filter.\n\nAn unrecognised `filter` (a typo, or an array value) is not an error: it\ndefaults to `assigned` and is disclosed via `meta.filter_ignored` /\n`meta.filter_note`, and the applied filter is echoed back in `filter`, so\na client never mistakes one tab's list for another's.\n\nClaim-pool rows are flagged `claimable: true` with a null `assignee`; a\nclient renders a Claim action for them instead of Done/Release.\n",
        "parameters": [
          {
            "name": "filter",
            "in": "query",
            "required": false,
            "description": "Which tab to return. Defaults to `assigned`.",
            "schema": {
              "type": "string",
              "enum": [
                "assigned",
                "overdue",
                "critical",
                "pool"
              ],
              "default": "assigned"
            }
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows per page (1–100, default 25).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The selected filter's page, all four counts, and pagination meta.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "filter": {
                      "type": "string",
                      "description": "The filter actually rendered (the normalised `filter`).",
                      "enum": [
                        "assigned",
                        "overdue",
                        "critical",
                        "pool"
                      ],
                      "example": "assigned"
                    },
                    "counts": {
                      "type": "object",
                      "description": "Exact size of all four filters (0 included). Filters overlap.",
                      "properties": {
                        "assigned": {
                          "type": "integer",
                          "example": 7
                        },
                        "overdue": {
                          "type": "integer",
                          "example": 2
                        },
                        "critical": {
                          "type": "integer",
                          "example": 1
                        },
                        "claim_pool": {
                          "type": "integer",
                          "example": 3
                        }
                      }
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MyDayListItem"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/MyDayListMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the caller lacks the required scope (`insufficient_permissions`), or the My Day surface is turned off (`surface_disabled`)."
          }
        }
      }
    },
    "/frontline_execution/my_day/everything": {
      "get": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "The worker's cross-app \"Everything\" list (My Day)",
        "description": "Returns one ranked, paginated list of everything the logged-in worker\nowes today across apps, plus the exact count of every filter lane so a\ntab bar renders from a single call.\n\n**`filter`** selects one lane. `all` (the default) is the whole union;\nevery other value is that one lane:\n\n| filter        | contains                                   |\n|---------------|--------------------------------------------|\n| `all`         | the whole deduped, ranked union            |\n| `frontline`   | Frontline Execution obligations            |\n| `inspections` | Inspections assigned to the worker         |\n| `posts`       | must-read messages awaiting acknowledgement|\n| `tasks`       | ordinary Tasks (not minted by Frontline)   |\n| `training`    | assigned/in-progress/overdue training      |\n| `schedule`    | shift offers addressed to the worker       |\n| `approvals`   | requests waiting on the worker's decision  |\n\n**`counts`** carries EVERY lane on every call (0 included), computed over\nthe whole ranked union — so a lane's badge equals what paging that lane\nactually returns. `counts.all` is the union total.\n\n**`degraded_sources`** names any lane whose owning app failed to load\n(returned in the same product vocabulary as `filter`). A non-empty array\nmeans the list is INCOMPLETE — a client must say so rather than present a\nsilently-short list as the whole day.\n\n**Bounded by design.** Each lane is capped server-side, so the union is a\nsmall, fixed size regardless of how much work the tenant has; the list is\nranked and paged in memory because it spans seven models with no shared\ntable or order key. Paging is stable across requests.\n\nAn unrecognised `filter` (a typo, or an array value) is not an error: it\ndefaults to `all` and is disclosed via `meta.filter_ignored` /\n`meta.filter_note`, so a client never mistakes the full list for a\nfiltered one.\n",
        "parameters": [
          {
            "name": "filter",
            "in": "query",
            "required": false,
            "description": "Which lane to return. Defaults to `all`.",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "frontline",
                "inspections",
                "posts",
                "tasks",
                "training",
                "schedule",
                "approvals"
              ],
              "default": "all"
            }
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows per page (1–100, default 25).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The selected lane, every lane's count, and pagination meta.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "filter": {
                      "type": "string",
                      "description": "The lane actually rendered (the normalised `filter`).",
                      "example": "all"
                    },
                    "counts": {
                      "type": "object",
                      "description": "Exact size of every lane over the whole union (0 included).",
                      "properties": {
                        "all": {
                          "type": "integer",
                          "example": 12
                        },
                        "frontline": {
                          "type": "integer",
                          "example": 4
                        },
                        "inspections": {
                          "type": "integer",
                          "example": 1
                        },
                        "posts": {
                          "type": "integer",
                          "example": 2
                        },
                        "tasks": {
                          "type": "integer",
                          "example": 3
                        },
                        "training": {
                          "type": "integer",
                          "example": 1
                        },
                        "schedule": {
                          "type": "integer",
                          "example": 0
                        },
                        "approvals": {
                          "type": "integer",
                          "example": 1
                        }
                      }
                    },
                    "degraded_sources": {
                      "type": "array",
                      "description": "Lanes whose owning app failed to load, in `filter` vocabulary. Non-empty ⇒ the list is incomplete.",
                      "items": {
                        "type": "string",
                        "enum": [
                          "frontline",
                          "inspections",
                          "posts",
                          "tasks",
                          "training",
                          "schedule",
                          "approvals"
                        ]
                      },
                      "example": []
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/UnifiedDayRow"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/UnifiedDayListMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the caller lacks the required scope (`insufficient_permissions`), or the My Day surface is turned off (`surface_disabled`)."
          }
        }
      }
    },
    "/frontline_execution/reviews": {
      "get": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "The reviewer's queue of submitted work, with proof inline",
        "description": "Returns the submitted work waiting on this reviewer's decision, oldest\nfirst, span-bounded (an admin/app admin sees the whole tenant; a manager\nonly their own stores). Each row carries the proof the reviewer decides\non — photos, signature, completion note and the cached AI vision verdict.\n\n- **`items`** — one page of the queue (see `ReviewItem`).\n- **`campaign_id`** — the applied campaign filter, or `null`.\n- **`campaign_options`** — `{ id, name }` for every campaign with work in\n  this reviewer's queue (plus the active filter), for the filter dropdown.\n- **`meta`** — the standard pagination envelope. When a `campaign_id` was\n  sent that this business has no campaign for, `campaign_filter_ignored`\n  is `true` and `campaign_filter_note` explains it (the queue is NOT\n  silently widened).\n",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            },
            "description": "Page number (1-based)."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            },
            "description": "Rows per page (clamped 1–100)."
          },
          {
            "name": "campaign_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Narrow the queue to one campaign. Tenant-checked; an unknown id is disclosed in `meta.campaign_filter_ignored` rather than widening."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of the review queue for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "campaign_id": {
                      "type": "integer",
                      "nullable": true,
                      "description": "The applied campaign filter, or null."
                    },
                    "campaign_options": {
                      "type": "array",
                      "description": "Campaigns with work in this queue, for the filter dropdown.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 42
                          },
                          "name": {
                            "type": "string",
                            "example": "Endcap reset — March"
                          }
                        }
                      }
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ReviewItem"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ReviewPaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the caller lacks the required scope (`insufficient_permissions`), the caller can't review work (`forbidden`), or the My Day surface is turned off (`surface_disabled`)."
          }
        }
      }
    },
    "/frontline_execution/reviews/ready_to_close": {
      "get": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Active campaigns whose work is fully resolved and ready to close",
        "description": "Returns the active campaigns this caller could close because every one\nof their obligations is resolved (done or missed) and none is still open,\nin progress or awaiting review. Span-bounded — an admin/app admin sees\nthe whole tenant, a manager only campaigns confined to their own stores.\n\n- **`campaigns`** — the ready-to-close campaigns (see `ReadyToCloseCampaign`).\n- **`total_count`** — how many are returned.\n- **`capped`** — `true` when the list was cut at the 100-row cap.\n\nUnpaginated by design (a settled-work list, naturally small).\n",
        "responses": {
          "200": {
            "description": "The campaigns ready to close for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "campaigns": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ReadyToCloseCampaign"
                      }
                    },
                    "total_count": {
                      "type": "integer",
                      "example": 3
                    },
                    "capped": {
                      "type": "boolean",
                      "example": false
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the caller lacks the required scope (`insufficient_permissions`), the caller can't review/manage campaigns (`forbidden`), or the My Day surface is turned off (`surface_disabled`)."
          }
        }
      }
    },
    "/frontline_execution/day_sheet": {
      "get": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "The manager's Day Sheet — filtered, paginated to-dos with all eight counts",
        "description": "Returns one page of the Day Sheet for the selected `filter`, the exact\nsize of ALL EIGHT filters (so the tab bar renders in one round-trip), and\nthe standard pagination envelope for the selected filter.\n\n**Filters**\n\n- `everything` (default) — everything on the sheet that isn't finished\n  (not done, not missed): the union of the five buckets below.\n- `escalated` — blocked work whose blocker group (campaign + reason) has\n  an OPEN escalation. Overlaps `blocked`.\n- `blocked` — a worker reported a problem they can't clear.\n- `unassigned` — nobody is on the hook yet (open, in the pool).\n- `sent_back` — a reviewer sent it back to be redone (reopened).\n- `in_review` — proof is in, waiting on a reviewer (submitted).\n- `newly_assigned` — someone holds it and is working on it.\n- `completed` — done.\n\nRows are ordered by triage rank (blocked → sent-back → unassigned →\nin-review → newly-assigned → completed) then soonest due then id — a\ntotal order, so paging is stable across requests.\n\n**Scoping & filters** — the sheet is span-bounded (an admin/app-admin\nsees the whole tenant, a manager only their own subtree). `location_id`\nnarrows to one span-checked store (absent it, the whole span);\n`category_id` narrows to one programme. An out-of-span `location_id` is a\n`404`; an unknown `category_id` is disclosed via\n`meta.category_filter_ignored` rather than silently widening. An\nunrecognised `filter` defaults to `everything` and is disclosed via\n`meta.filter_ignored` / `meta.filter_note`.\n\nThis is a **manager** surface — the same audience the web Day Sheet is\ngated to. A plain member is refused with `403 forbidden`.\n",
        "parameters": [
          {
            "name": "filter",
            "in": "query",
            "required": false,
            "description": "Which tab to return. Defaults to `everything`.",
            "schema": {
              "type": "string",
              "enum": [
                "everything",
                "escalated",
                "blocked",
                "unassigned",
                "sent_back",
                "in_review",
                "newly_assigned",
                "completed"
              ],
              "default": "everything"
            }
          },
          {
            "name": "location_id",
            "in": "query",
            "required": false,
            "description": "Narrow the sheet to one store (must be an active physical site inside the caller's span; `404` otherwise). Absent, the whole span.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "category_id",
            "in": "query",
            "required": false,
            "description": "Narrow the sheet to one campaign category (programme). An unknown id is ignored and disclosed in `meta.category_filter_ignored`.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows per page (1–100, default 25).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The selected filter's page, all eight counts, and pagination meta.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "filter": {
                      "type": "string",
                      "description": "The filter actually rendered (the normalised `filter`).",
                      "enum": [
                        "everything",
                        "escalated",
                        "blocked",
                        "unassigned",
                        "sent_back",
                        "in_review",
                        "newly_assigned",
                        "completed"
                      ],
                      "example": "everything"
                    },
                    "location_id": {
                      "type": "integer",
                      "nullable": true,
                      "description": "The applied location filter, or null for the whole span.",
                      "example": 3369
                    },
                    "category_id": {
                      "type": "integer",
                      "nullable": true,
                      "description": "The applied category filter, or null.",
                      "example": 42
                    },
                    "counts": {
                      "$ref": "#/components/schemas/DaySheetCounts"
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DaySheetListItem"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/DaySheetListMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the caller lacks the required scope (`insufficient_permissions`), the caller can't manage the Day Sheet (`forbidden`), or the My Day surface is turned off (`surface_disabled`)."
          },
          "404": {
            "description": "The requested `location_id` is not found or is outside the caller's area."
          }
        }
      }
    },
    "/frontline_execution/day_sheet/filters": {
      "get": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "The Day Sheet's category and store filter options",
        "description": "Returns the two dropdowns the Day Sheet filters on:\n\n- **`categories`** — every campaign category in the business, ordered as\n  the picker shows them (sort_order, then name). RETIRED (inactive)\n  categories are included on purpose, flagged `active: false`, so a sheet\n  narrowed to a since-retired programme still resolves. Identical for\n  every caller who can reach the sheet.\n\n- **`locations`** — the active, physical stores this caller may open the\n  sheet for, ordered by name, each as `{ id, name }`. Administrative\n  rollup nodes (regions/divisions) and deactivated stores are excluded.\n  This list is location-scoped: an admin / app admin sees every store in\n  the tenant, a manager only the stores in their own subtree.\n\n- **`locations_meta`** — the store list's disclosure. `total` is the true\n  store count in the caller's span; `shown` is how many are returned;\n  `has_more` is `true` when the list was cut at the picker cap (2,000);\n  `deactivated_only` is `true` when the span holds physical stores but\n  every one is switched off (so a client shows \"your stores are\n  deactivated\" rather than \"no stores imported\").\n\nThis is a **manager** surface — the same audience the web Day Sheet is\ngated to. A plain member is refused with `403 forbidden`.\n",
        "responses": {
          "200": {
            "description": "The category and store filter options for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "categories": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DaySheetCategory"
                      }
                    },
                    "locations": {
                      "type": "array",
                      "description": "Active physical stores in the caller's span, ordered by name.",
                      "items": {
                        "$ref": "#/components/schemas/DaySheetStore"
                      }
                    },
                    "locations_meta": {
                      "$ref": "#/components/schemas/DaySheetLocationsMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the caller lacks the required scope (`insufficient_permissions`), the caller can't manage the Day Sheet (`forbidden`), or the My Day surface is turned off (`surface_disabled`)."
          }
        }
      }
    },
    "/frontline_execution/day_sheet/what_changed": {
      "get": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "What changed at one store since a chosen point (the shift-change brief)",
        "description": "Returns the \"what changed\" brief for ONE store across a chosen look-back\nwindow — the same six buckets the web Day Sheet's \"What changed\" card\nrenders, in the order a manager acts on them.\n\n**Buckets** (`changes`):\n\n| bucket           | what it holds                                            |\n|------------------|----------------------------------------------------------|\n| `blocked`        | work flagged \"I can't do this\" in the window (with reason)|\n| `sent_back`      | work reopened for rework in the window                   |\n| `escalated`      | work escalated in the window (distinct items, not events)|\n| `newly_assigned` | work that landed on somebody in the window               |\n| `new_in_pool`    | unclaimed work that appeared in the claim pool           |\n| `completed`      | work finished in the window                              |\n\nEach bucket is capped at `bucket_limit` (10) and SAYS SO when it hits the\ncap via `summary.<bucket>.capped` — a truncated list is never presented as\ncomplete. `completed` and `escalated` also carry a true `total` (the two\ncounted buckets), so a client can render \"showing the 10 most recent of N\".\n\n**`since_hours`** is snapped to the offered windows (4/8/12/24/48/72),\ndefaulting to 12. An unrecognised value defaults to 12 and is disclosed via\n`window.since_hours_ignored` / `window.since_hours_note`, so a client never\nrenders a 12-hour brief under a label it did not ask for. `window.options`\nadvertises the pickable windows so a client can render the selector.\n\n**`location_id` is required** — the brief is per-store. A missing param is\n`422 invalid_request`; a store that is closed, an administrative (rollup)\nnode, outside the caller's span, or gone is `404 not_found` (never a silent\nwidening to the whole span).\n\n`any_changes` is false when nothing moved in the window — distinct from a\nstore with no work at all. Rows are titled by campaign name and carry the\ncurrent holder (null for claim-pool rows).\n",
        "parameters": [
          {
            "name": "location_id",
            "in": "query",
            "required": true,
            "description": "The store to brief on. Must be an active physical site inside the caller's span.",
            "schema": {
              "type": "integer"
            },
            "example": 8
          },
          {
            "name": "since_hours",
            "in": "query",
            "required": false,
            "description": "Look-back window in hours. Snapped to one of 4/8/12/24/48/72; defaults to 12.",
            "schema": {
              "type": "integer",
              "enum": [
                4,
                8,
                12,
                24,
                48,
                72
              ],
              "default": 12
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The store's what-changed brief for the selected window.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "location": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 8
                        },
                        "name": {
                          "type": "string",
                          "example": "Fourth Street"
                        }
                      }
                    },
                    "window": {
                      "$ref": "#/components/schemas/ShiftBriefWindow"
                    },
                    "any_changes": {
                      "type": "boolean",
                      "description": "Whether anything moved in the window (distinct from \"no work at all\").",
                      "example": true
                    },
                    "bucket_limit": {
                      "type": "integer",
                      "description": "The per-bucket render cap; a bucket at this size may hold more.",
                      "example": 10
                    },
                    "summary": {
                      "type": "object",
                      "description": "Count + cap state per bucket. `completed`/`escalated` also carry a true `total` past the cap.",
                      "additionalProperties": {
                        "$ref": "#/components/schemas/ShiftBriefBucketSummary"
                      },
                      "example": {
                        "newly_assigned": {
                          "count": 3,
                          "capped": false
                        },
                        "new_in_pool": {
                          "count": 1,
                          "capped": false
                        },
                        "blocked": {
                          "count": 2,
                          "capped": false
                        },
                        "sent_back": {
                          "count": 0,
                          "capped": false
                        },
                        "completed": {
                          "count": 10,
                          "capped": true,
                          "total": 43
                        },
                        "escalated": {
                          "count": 1,
                          "capped": false,
                          "total": 1
                        }
                      }
                    },
                    "changes": {
                      "type": "object",
                      "description": "The six expandable lists, in the order the card reads them.",
                      "properties": {
                        "blocked": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/ShiftBriefBlockedRow"
                          }
                        },
                        "sent_back": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/ShiftBriefRow"
                          }
                        },
                        "escalated": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/ShiftBriefRow"
                          }
                        },
                        "newly_assigned": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/ShiftBriefRow"
                          }
                        },
                        "new_in_pool": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/ShiftBriefRow"
                          }
                        },
                        "completed": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/ShiftBriefCompletedRow"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the caller is not a manager (`forbidden`), the caller lacks the required scope (`insufficient_permissions`), or the My Day surface is turned off (`surface_disabled`)."
          },
          "404": {
            "description": "The store was not found or is outside the caller's span (`not_found`) — closed, an administrative node, or in another tenant."
          },
          "422": {
            "description": "`location_id` was not supplied (`invalid_request`)."
          }
        }
      }
    },
    "/frontline_execution/passdown": {
      "get": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Read the current shift pass-down for a location",
        "description": "Returns the handover the incoming shift should read, or `null` when\nnothing was handed over inside the currency window (24h).\n\n**`passdown.status` matters.** A `machine_closed` record means the last\nshift was ended by the system — auto clock-out, a punch-device sync, a\nCSV import — so nobody was ever prompted for a handover. Clients MUST\nrender that differently from `passdown: null`: the first means \"nobody\nwas asked\", the second means \"nothing was handed over\". Showing a blank\nall-clear card for a machine-closed shift is the failure this endpoint\nexists to prevent.\n",
        "parameters": [
          {
            "name": "location_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "A physical location inside the caller's span."
          }
        ],
        "responses": {
          "200": {
            "description": "The current pass-down, or null.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "location_id": {
                      "type": "integer",
                      "example": 1118
                    },
                    "passdown": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/ShiftPassdown"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Pass-downs are turned off for this business."
          },
          "404": {
            "description": "Location not found or outside the caller's span."
          }
        }
      },
      "post": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Record a shift pass-down",
        "description": "Files a handover against a location. The newest pass-down supersedes\nearlier ones for that site; previous records remain as history.\n\nA submission with neither `notes` nor any `structured` value is refused\nwith `422 empty` — an empty handover tells the next shift nothing, and\nrecording one would let a site look covered when it is not.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "location_id"
                ],
                "properties": {
                  "location_id": {
                    "type": "integer",
                    "example": 1118
                  },
                  "notes": {
                    "type": "string",
                    "example": "Bed 12 family conversation still open. Cold chain checked at 6pm."
                  },
                  "structured": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "Free key/value counts. The instrument differs per tenant and per site.",
                    "example": {
                      "headcount": "14",
                      "open_items": "2"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Pass-down recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "passdown": {
                      "$ref": "#/components/schemas/ShiftPassdown"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Pass-downs are turned off for this business."
          },
          "404": {
            "description": "Location not found or outside the caller's span."
          },
          "422": {
            "description": "location_id missing, empty submission, or the record failed validation."
          }
        }
      }
    },
    "/frontline_execution/passdown/{id}/acknowledge": {
      "post": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Acknowledge a shift pass-down",
        "description": "Records that the incoming shift has read the handover. Idempotent — a\nsecond call on an already-acknowledged record succeeds and leaves the\noriginal reader and timestamp intact.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Acknowledged (or already was).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "passdown": {
                      "$ref": "#/components/schemas/ShiftPassdown"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Pass-downs are turned off for this business."
          },
          "404": {
            "description": "Handover not found or outside the caller's span."
          }
        }
      }
    },
    "/frontline_execution/items/{id}": {
      "get": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "One obligation in full (adapts to the caller's lens)",
        "description": "Returns everything the web detail and proof surfaces show for one\nobligation, adapted to who is asking:\n\n- **`item`** — the common core (identity, badges, requirements,\n  instruction, documents, holder/accountable, blocker/send-back context,\n  challenge), plus lens-gated sections:\n  - `assignment_history` and `review_proof` — for a **reviewer** (a\n    manager/admin whose span covers the location). `review_proof` is\n    present only for work sitting in `submitted`.\n  - `capture` and `assignment_explanation` — for the **holder** (or an\n    acknowledgement roster member): what's already captured, the shared\n    note trail, and why the work landed on them.\n- **`viewer`** — the caller's lens (`is_holder` / `is_reviewer`) and a\n  `can` map of the six item actions they may take (mirrors the web's own\n  \"show this button\" truth; the write endpoints remain the authority).\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The obligation (CampaignItem) id.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The obligation, with the sections the caller is entitled to.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "item": {
                      "$ref": "#/components/schemas/FrontlineItemDetail"
                    },
                    "viewer": {
                      "$ref": "#/components/schemas/FrontlineItemViewer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the caller lacks the required scope (`insufficient_permissions`), the My Day surface is turned off (`surface_disabled`), or the caller can't see this row (`forbidden`)."
          },
          "404": {
            "description": "No such obligation in this business (`not_found`)."
          }
        }
      }
    },
    "/frontline_execution/requests": {
      "get": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "The Requests tab — the caller's asks, or the approval queue, filtered and paged",
        "description": "Returns one page of intake requests for the selected `filter`, plus every\nvisible tab's count and the caller's review capability.\n\n- **`filter`** — the APPLIED filter (`all` / `submitted` / `approved` /\n  `declined` / `approval`). Differs from the requested one when that was\n  unknown or not available to the caller — see `meta.filter_ignored`.\n- **`can_review`** — whether the caller is a campaign-author gatekeeper (so\n  the client draws the \"For approval\" tab and reads `counts.approval`).\n- **`counts`** — every visible tab's badge in one round-trip. `all` /\n  `submitted` / `approved` / `declined` count the caller's OWN asks;\n  `approval` (reviewers only) counts every pending ask in the business.\n- **`requests`** — one page of rows. Each row is the full request (same\n  shape as the detail endpoint) plus a `viewer` block whose flags mirror\n  the web buttons (`can_withdraw` on the caller's own pending ask;\n  `can_approve` / `can_decline` for a gatekeeper on a pending, non-engine\n  request).\n- **`meta`** — the standard pagination envelope. When the requested filter\n  was not applied, `filter_ignored` is `true` and `filter_note` says why\n  (a typo, or `approval` asked for by a non-reviewer) — the list is NOT\n  silently widened; it falls back to `all`.\n",
        "parameters": [
          {
            "name": "filter",
            "in": "query",
            "required": false,
            "description": "Which lens to read: `all` (default), `submitted` (\"In review\"), `approved`, `declined` — the caller's own asks — or `approval`, the reviewer-only queue of everyone's pending asks.",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "approval",
                "submitted",
                "approved",
                "declined"
              ],
              "default": "all"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            },
            "description": "Page number (1-based)."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            },
            "description": "Rows per page (clamped 1–100)."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of requests for the selected filter, with counts and the caller's capability.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "filter": {
                      "type": "string",
                      "description": "The applied filter (may differ from the requested one — see meta).",
                      "example": "all"
                    },
                    "can_review": {
                      "type": "boolean",
                      "description": "The caller is a campaign-author gatekeeper (the \"For approval\" tab is theirs).",
                      "example": false
                    },
                    "counts": {
                      "$ref": "#/components/schemas/FrontlineRequestCounts"
                    },
                    "requests": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FrontlineRequestListRow"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/FrontlineRequestListMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the caller lacks the required scope (`insufficient_permissions`), or the Campaigns surface is turned off (`surface_disabled`)."
          }
        }
      },
      "post": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Submit an intake request (the \"New request\" form)",
        "description": "Submits a campaign-intake request — the API twin of the web \"New request\"\nform every persona reaches from the Requests tab. EVERY member may file one\n(\"stores can ask too\"), so this rides the own-scoped write tier with no\nauthor gate; only the app + Campaigns-surface toggles apply.\n\nThe request routes through `Execution::CampaignRequestCreator`, the SAME\nwriter the web form uses, so a phone submission and a browser submission are\nidentical: cross-tenant FK guards on department/audience/category, store-list\nresolution (multi-select ids INTERSECTED with the tenant's physical sites,\nplus pasted/uploaded store numbers), out-of-band attachment, and decision\nrouting (into the tenant's configured approval workflow when one exists, else\na notification to the gatekeepers).\n\nOn success returns **201** with the created request in the same\n`{request, viewer}` shape the detail endpoint returns, plus:\n- **`store_list`** — how many pasted store numbers matched and which didn't\n  (present only when a store list was pasted).\n- **`warnings`** — a file the request couldn't attach (reported, not fatal;\n  the ask itself is saved), present only when there is something to say.\n\nA validation failure is **422** whose `error.details.errors` names the\noffending fields.\n\nSend `application/json` for a plain request, or `multipart/form-data` when\nattaching files (`attachments[]`) or uploading a store-number CSV\n(`store_list_paste_file`).\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FrontlineRequestCreateBody"
              }
            },
            "multipart/form-data": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/FrontlineRequestCreateBody"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "attachments": {
                        "type": "array",
                        "description": "Reference files for the ask (images/PDFs/docs; oversized or unsupported files are reported in `warnings`, not fatal).",
                        "items": {
                          "type": "string",
                          "format": "binary"
                        }
                      },
                      "store_list_paste_file": {
                        "type": "string",
                        "format": "binary",
                        "description": "A CSV/text file of store numbers, resolved the same way as `store_list_paste`."
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The request was submitted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "request": {
                      "type": "object",
                      "description": "One intake request in full. Decision/campaign fields fill in as it moves through the funnel.",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 31
                        },
                        "title": {
                          "type": "string",
                          "example": "Re-check cold chain after firmware fix"
                        },
                        "status": {
                          "type": "string",
                          "description": "submitted / approved / declined / withdrawn.",
                          "example": "submitted"
                        },
                        "priority": {
                          "type": "string",
                          "example": "important"
                        },
                        "work_type": {
                          "type": "string",
                          "nullable": true,
                          "example": "task"
                        },
                        "work_type_label": {
                          "type": "string",
                          "nullable": true,
                          "example": "Task"
                        },
                        "instructions": {
                          "type": "string",
                          "nullable": true,
                          "example": "Read both pharmacy fridges again and photograph the panel."
                        },
                        "note": {
                          "type": "string",
                          "nullable": true
                        },
                        "estimated_duration_minutes": {
                          "type": "integer",
                          "nullable": true,
                          "example": 15
                        },
                        "desired_start_on": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "desired_end_on": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "desired_publish_on": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "category": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 7
                            },
                            "name": {
                              "type": "string",
                              "example": "Compliance"
                            }
                          }
                        },
                        "department": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 4
                            },
                            "name": {
                              "type": "string",
                              "example": "Pharmacy"
                            }
                          }
                        },
                        "audience": {
                          "type": "object",
                          "nullable": true,
                          "description": "The requested audience, or null (\"Reviewer's call\" on the web).",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 12
                            },
                            "name": {
                              "type": "string",
                              "example": "5 pharmacy stores"
                            }
                          }
                        },
                        "target_location_count": {
                          "type": "integer",
                          "description": "How many stores the requester named directly (0 when they named an audience or left it to ops).",
                          "example": 0
                        },
                        "requested_by": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 55
                            },
                            "name": {
                              "type": "string",
                              "example": "Anthony Rivera"
                            }
                          }
                        },
                        "requested_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "decided_by": {
                          "type": "object",
                          "nullable": true,
                          "description": "Who decided it — present once approved or declined.",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 3
                            },
                            "name": {
                              "type": "string",
                              "example": "Dana Okafor"
                            }
                          }
                        },
                        "decided_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "decline_reason": {
                          "type": "string",
                          "nullable": true,
                          "description": "The reason that travels back to the requester — present only on a declined request."
                        },
                        "under_engine_review": {
                          "type": "boolean",
                          "description": "The request is sitting in a configured approval workflow (decided in the Approvals surface, not the queue's buttons).",
                          "example": false
                        },
                        "campaign": {
                          "type": "object",
                          "nullable": true,
                          "description": "The draft campaign the approval minted — present only once approved.",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 88
                            },
                            "name": {
                              "type": "string",
                              "example": "Re-check cold chain after firmware fix"
                            },
                            "status": {
                              "type": "string",
                              "example": "draft"
                            }
                          }
                        },
                        "attachments": {
                          "type": "array",
                          "description": "The requester's files (expiring download URLs).",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 91
                              },
                              "filename": {
                                "type": "string",
                                "example": "planogram.pdf"
                              },
                              "byte_size": {
                                "type": "integer",
                                "example": 220100
                              },
                              "content_type": {
                                "type": "string",
                                "example": "application/pdf"
                              },
                              "url": {
                                "type": "string",
                                "example": "https://.../rails/active_storage/..."
                              }
                            }
                          }
                        }
                      }
                    },
                    "viewer": {
                      "type": "object",
                      "description": "The caller's lens on this intake request and the actions they may take.",
                      "properties": {
                        "is_requester": {
                          "type": "boolean",
                          "description": "The caller submitted this request.",
                          "example": false
                        },
                        "is_gatekeeper": {
                          "type": "boolean",
                          "description": "The caller is in the campaign-author tier that works the queue.",
                          "example": true
                        },
                        "can_approve": {
                          "type": "boolean",
                          "description": "Show the Approve control — the caller is a gatekeeper, the request is still pending, and no configured approval workflow owns the decision.",
                          "example": true
                        },
                        "can_decline": {
                          "type": "boolean",
                          "description": "Show the Decline control — same condition as can_approve.",
                          "example": true
                        },
                        "can_withdraw": {
                          "type": "boolean",
                          "description": "Show the Withdraw control — the caller is the submitter and the request is still pending.",
                          "example": false
                        }
                      }
                    },
                    "store_list": {
                      "$ref": "#/components/schemas/FrontlineRequestStoreListOutcome"
                    },
                    "warnings": {
                      "type": "array",
                      "description": "Non-fatal notices (e.g. a file that couldn't be attached). Present only when non-empty.",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the caller lacks the required scope (`insufficient_permissions`), or the Campaigns surface is turned off (`surface_disabled`)."
          },
          "422": {
            "description": "The request was refused (`invalid_request`). `error.details.errors` maps each offending field to its messages."
          }
        }
      }
    },
    "/frontline_execution/requests/{id}": {
      "get": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "One intake request in full (adapts to the caller's lens)",
        "description": "Returns everything the web request detail shows for one intake request,\nadapted to who is asking:\n\n- **`request`** — the ask itself (title, instructions, priority, work\n  type, department, audience, category, desired window, estimated\n  effort, target-store count, attachments), the decision trail\n  (`decided_by` / `decided_at` / `decline_reason`), whether it is routed\n  through a configured approval workflow (`under_engine_review`), and the\n  draft campaign it minted once approved (`campaign`).\n- **`viewer`** — the caller's lens (`is_requester` / `is_gatekeeper`) and\n  the three request-action flags (`can_approve`, `can_decline`,\n  `can_withdraw`). These mirror the web's own \"show this button\" truth;\n  the write endpoints remain the authority.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The intake request (Execution::CampaignRequest) id.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The request, with the viewer block for the caller.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "request": {
                      "$ref": "#/components/schemas/FrontlineRequestDetail"
                    },
                    "viewer": {
                      "$ref": "#/components/schemas/FrontlineRequestViewer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the caller lacks the required scope (`insufficient_permissions`), the Campaigns surface is turned off (`surface_disabled`), or the caller is neither the submitter nor a gatekeeper (`forbidden`)."
          },
          "404": {
            "description": "No such request in this business (`not_found`)."
          }
        }
      }
    },
    "/frontline_execution/requests/{id}/approve": {
      "post": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Approve an intake request (mints a draft campaign)",
        "description": "Approves the request: it moves to `approved`, the caller is stamped as\n`decided_by`, and a pre-filled DRAFT `Execution::Campaign` is minted from\nthe ask and linked as `request.campaign`.\n\nReturns the decided request in the same `{request, viewer}` shape the\ndetail endpoint returns — now approved (so `viewer.can_approve` is false)\nand carrying the minted draft under `request.campaign`, so a client can\nrender the new state and open the draft without a second round-trip.\n\nRefused (`409`) when the request was already decided or is no longer\npending (`already_decided` — including losing a race to another gatekeeper\nor the approval engine), or when a configured approval workflow owns the\ndecision (`engine_owned`). Refused (`422`, `invalid`) when the draft can't\nbe built.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The intake request (Execution::CampaignRequest) to approve."
          }
        ],
        "responses": {
          "200": {
            "description": "Approved. Returns the decided request and the caller's viewer block.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "request": {
                      "type": "object",
                      "description": "One intake request in full. Decision/campaign fields fill in as it moves through the funnel.",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 31
                        },
                        "title": {
                          "type": "string",
                          "example": "Re-check cold chain after firmware fix"
                        },
                        "status": {
                          "type": "string",
                          "description": "submitted / approved / declined / withdrawn.",
                          "example": "submitted"
                        },
                        "priority": {
                          "type": "string",
                          "example": "important"
                        },
                        "work_type": {
                          "type": "string",
                          "nullable": true,
                          "example": "task"
                        },
                        "work_type_label": {
                          "type": "string",
                          "nullable": true,
                          "example": "Task"
                        },
                        "instructions": {
                          "type": "string",
                          "nullable": true,
                          "example": "Read both pharmacy fridges again and photograph the panel."
                        },
                        "note": {
                          "type": "string",
                          "nullable": true
                        },
                        "estimated_duration_minutes": {
                          "type": "integer",
                          "nullable": true,
                          "example": 15
                        },
                        "desired_start_on": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "desired_end_on": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "desired_publish_on": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "category": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 7
                            },
                            "name": {
                              "type": "string",
                              "example": "Compliance"
                            }
                          }
                        },
                        "department": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 4
                            },
                            "name": {
                              "type": "string",
                              "example": "Pharmacy"
                            }
                          }
                        },
                        "audience": {
                          "type": "object",
                          "nullable": true,
                          "description": "The requested audience, or null (\"Reviewer's call\" on the web).",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 12
                            },
                            "name": {
                              "type": "string",
                              "example": "5 pharmacy stores"
                            }
                          }
                        },
                        "target_location_count": {
                          "type": "integer",
                          "description": "How many stores the requester named directly (0 when they named an audience or left it to ops).",
                          "example": 0
                        },
                        "requested_by": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 55
                            },
                            "name": {
                              "type": "string",
                              "example": "Anthony Rivera"
                            }
                          }
                        },
                        "requested_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "decided_by": {
                          "type": "object",
                          "nullable": true,
                          "description": "Who decided it — present once approved or declined.",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 3
                            },
                            "name": {
                              "type": "string",
                              "example": "Dana Okafor"
                            }
                          }
                        },
                        "decided_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "decline_reason": {
                          "type": "string",
                          "nullable": true,
                          "description": "The reason that travels back to the requester — present only on a declined request."
                        },
                        "under_engine_review": {
                          "type": "boolean",
                          "description": "The request is sitting in a configured approval workflow (decided in the Approvals surface, not the queue's buttons).",
                          "example": false
                        },
                        "campaign": {
                          "type": "object",
                          "nullable": true,
                          "description": "The draft campaign the approval minted — present only once approved.",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 88
                            },
                            "name": {
                              "type": "string",
                              "example": "Re-check cold chain after firmware fix"
                            },
                            "status": {
                              "type": "string",
                              "example": "draft"
                            }
                          }
                        },
                        "attachments": {
                          "type": "array",
                          "description": "The requester's files (expiring download URLs).",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 91
                              },
                              "filename": {
                                "type": "string",
                                "example": "planogram.pdf"
                              },
                              "byte_size": {
                                "type": "integer",
                                "example": 220100
                              },
                              "content_type": {
                                "type": "string",
                                "example": "application/pdf"
                              },
                              "url": {
                                "type": "string",
                                "example": "https://.../rails/active_storage/..."
                              }
                            }
                          }
                        }
                      }
                    },
                    "viewer": {
                      "type": "object",
                      "description": "The caller's lens on this intake request and the actions they may take.",
                      "properties": {
                        "is_requester": {
                          "type": "boolean",
                          "description": "The caller submitted this request.",
                          "example": false
                        },
                        "is_gatekeeper": {
                          "type": "boolean",
                          "description": "The caller is in the campaign-author tier that works the queue.",
                          "example": true
                        },
                        "can_approve": {
                          "type": "boolean",
                          "description": "Show the Approve control — the caller is a gatekeeper, the request is still pending, and no configured approval workflow owns the decision.",
                          "example": true
                        },
                        "can_decline": {
                          "type": "boolean",
                          "description": "Show the Decline control — same condition as can_approve.",
                          "example": true
                        },
                        "can_withdraw": {
                          "type": "boolean",
                          "description": "Show the Withdraw control — the caller is the submitter and the request is still pending.",
                          "example": false
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the token lacks `write:execution` (`insufficient_permissions`), the Campaigns surface is off (`surface_disabled`), or the caller is not in the campaign-author gatekeeper tier (`forbidden`)."
          },
          "404": {
            "description": "No such request in this business (`not_found`)."
          },
          "409": {
            "description": "The request can't be decided here: `already_decided` (already decided, no longer pending, or a concurrent decision won) or `engine_owned` (a configured approval workflow owns the decision).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RequestDecisionError"
                }
              }
            }
          },
          "422": {
            "description": "The draft campaign could not be built (`invalid`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RequestDecisionError"
                }
              }
            }
          }
        }
      }
    },
    "/frontline_execution/requests/{id}/decline": {
      "post": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Decline an intake request (reason required)",
        "description": "Declines the request: it moves to `declined`, the caller is stamped as\n`decided_by`, and the `decline_reason` is stored and sent back to the\nrequester.\n\nThe `decline_reason` is **required** — it is the only thing the person who\nasked sees, so a reason-free decline leaves them nothing to fix and\nresubmit. A blank, non-string, or shorter-than-4-character reason is\nrefused with `reason_required` and the request stays pending.\n\nReturns the decided request in the `{request, viewer}` shape (now declined,\ncarrying the `decline_reason`).\n\nRefused (`409`) when already decided (`already_decided`) or owned by a\nconfigured approval workflow (`engine_owned`).\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The intake request (Execution::CampaignRequest) to decline."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "decline_reason"
                ],
                "properties": {
                  "decline_reason": {
                    "type": "string",
                    "minLength": 4,
                    "description": "Why the request is declined (required, ≥ 4 chars). Shown to the requester on their request and in the notification.",
                    "example": "Already covered by a live campaign — no separate work needed."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Declined. Returns the decided request and the caller's viewer block.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "request": {
                      "type": "object",
                      "description": "One intake request in full. Decision/campaign fields fill in as it moves through the funnel.",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 31
                        },
                        "title": {
                          "type": "string",
                          "example": "Re-check cold chain after firmware fix"
                        },
                        "status": {
                          "type": "string",
                          "description": "submitted / approved / declined / withdrawn.",
                          "example": "submitted"
                        },
                        "priority": {
                          "type": "string",
                          "example": "important"
                        },
                        "work_type": {
                          "type": "string",
                          "nullable": true,
                          "example": "task"
                        },
                        "work_type_label": {
                          "type": "string",
                          "nullable": true,
                          "example": "Task"
                        },
                        "instructions": {
                          "type": "string",
                          "nullable": true,
                          "example": "Read both pharmacy fridges again and photograph the panel."
                        },
                        "note": {
                          "type": "string",
                          "nullable": true
                        },
                        "estimated_duration_minutes": {
                          "type": "integer",
                          "nullable": true,
                          "example": 15
                        },
                        "desired_start_on": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "desired_end_on": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "desired_publish_on": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "category": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 7
                            },
                            "name": {
                              "type": "string",
                              "example": "Compliance"
                            }
                          }
                        },
                        "department": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 4
                            },
                            "name": {
                              "type": "string",
                              "example": "Pharmacy"
                            }
                          }
                        },
                        "audience": {
                          "type": "object",
                          "nullable": true,
                          "description": "The requested audience, or null (\"Reviewer's call\" on the web).",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 12
                            },
                            "name": {
                              "type": "string",
                              "example": "5 pharmacy stores"
                            }
                          }
                        },
                        "target_location_count": {
                          "type": "integer",
                          "description": "How many stores the requester named directly (0 when they named an audience or left it to ops).",
                          "example": 0
                        },
                        "requested_by": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 55
                            },
                            "name": {
                              "type": "string",
                              "example": "Anthony Rivera"
                            }
                          }
                        },
                        "requested_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "decided_by": {
                          "type": "object",
                          "nullable": true,
                          "description": "Who decided it — present once approved or declined.",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 3
                            },
                            "name": {
                              "type": "string",
                              "example": "Dana Okafor"
                            }
                          }
                        },
                        "decided_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "decline_reason": {
                          "type": "string",
                          "nullable": true,
                          "description": "The reason that travels back to the requester — present only on a declined request."
                        },
                        "under_engine_review": {
                          "type": "boolean",
                          "description": "The request is sitting in a configured approval workflow (decided in the Approvals surface, not the queue's buttons).",
                          "example": false
                        },
                        "campaign": {
                          "type": "object",
                          "nullable": true,
                          "description": "The draft campaign the approval minted — present only once approved.",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 88
                            },
                            "name": {
                              "type": "string",
                              "example": "Re-check cold chain after firmware fix"
                            },
                            "status": {
                              "type": "string",
                              "example": "draft"
                            }
                          }
                        },
                        "attachments": {
                          "type": "array",
                          "description": "The requester's files (expiring download URLs).",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 91
                              },
                              "filename": {
                                "type": "string",
                                "example": "planogram.pdf"
                              },
                              "byte_size": {
                                "type": "integer",
                                "example": 220100
                              },
                              "content_type": {
                                "type": "string",
                                "example": "application/pdf"
                              },
                              "url": {
                                "type": "string",
                                "example": "https://.../rails/active_storage/..."
                              }
                            }
                          }
                        }
                      }
                    },
                    "viewer": {
                      "type": "object",
                      "description": "The caller's lens on this intake request and the actions they may take.",
                      "properties": {
                        "is_requester": {
                          "type": "boolean",
                          "description": "The caller submitted this request.",
                          "example": false
                        },
                        "is_gatekeeper": {
                          "type": "boolean",
                          "description": "The caller is in the campaign-author tier that works the queue.",
                          "example": true
                        },
                        "can_approve": {
                          "type": "boolean",
                          "description": "Show the Approve control — the caller is a gatekeeper, the request is still pending, and no configured approval workflow owns the decision.",
                          "example": true
                        },
                        "can_decline": {
                          "type": "boolean",
                          "description": "Show the Decline control — same condition as can_approve.",
                          "example": true
                        },
                        "can_withdraw": {
                          "type": "boolean",
                          "description": "Show the Withdraw control — the caller is the submitter and the request is still pending.",
                          "example": false
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the token lacks `write:execution` (`insufficient_permissions`), the Campaigns surface is off (`surface_disabled`), or the caller is not in the campaign-author gatekeeper tier (`forbidden`)."
          },
          "404": {
            "description": "No such request in this business (`not_found`)."
          },
          "409": {
            "description": "The request can't be decided here: `already_decided` or `engine_owned` (a configured approval workflow owns the decision).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RequestDecisionError"
                }
              }
            }
          },
          "422": {
            "description": "The decline was refused: `reason_required` (no/blank/too-short reason) or `invalid`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RequestDecisionError"
                }
              }
            }
          }
        }
      }
    },
    "/frontline_execution/items/{id}/claim": {
      "post": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Claim (take) an unclaimed pool item",
        "description": "Binds the caller to an unclaimed obligation and takes it out of the claim\npool: the item moves to `claimed`, its `current_assignee` becomes the\ncaller, `claimed_at` is stamped, a `self_claim` event is written to the\nitem's history, and the fulfillment the campaign describes (a Task, or an\nInspection for an inspection campaign) is minted into the worker's list.\n\nOnly a caller who MAY claim the item reaches the transition. The item must\nbe open and unassigned, its campaign must be active, the caller must be\nmapped to the item's location, and — when the campaign restricts the work\nto a role — the caller must hold that role. A caller who fails any of\nthese is refused `403` before anything is written (\"This work isn't\navailable for you to claim.\").\n\nThe claim is contention-safe: if another worker claims the same item in\nthe same instant, the loser is refused `422 already_assigned` (\"Someone\nelse just claimed this work.\") rather than silently displacing the winner.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The campaign item (pool obligation) to claim."
          }
        ],
        "responses": {
          "200": {
            "description": "Claimed. Returns the updated item (now `claimed`, assigned to the caller).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "item": {
                      "$ref": "#/components/schemas/ClaimedItem"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the token lacks a write scope (`insufficient_permissions`), the My Day surface is off (`surface_disabled`), or the caller may not claim this item (`forbidden` — \"This work isn't available for you to claim.\": already assigned, campaign not active, wrong location, or missing the required role)."
          },
          "404": {
            "description": "No such item in this business (`not_found`)."
          },
          "422": {
            "description": "The claim was lost to a concurrent claimer (`already_assigned`), the work is no longer open (`not_claimable`), or the worker is at their critical-work cap.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClaimError"
                }
              }
            }
          }
        }
      }
    },
    "/frontline_execution/items/{id}/complete": {
      "post": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Save and mark an obligation done (with photo / signature / note proof)",
        "description": "Marks the work done, capturing any submitted proof first.\n\n**Proof.** When the campaign demands a photo (`require_photo`) or a\nsignature (`require_signature`), the matching proof must be present on the\nrecord before the work can finish — otherwise the completion is refused\nwith `proof_required` and nothing changes. Submit photos as `photos[]`\n(multipart), a signature as a `signature_data` data URL, and an optional\n`completion_note`. Proof capture is **partial**: a photo the server refuses\n(not an image, over 10 MB, or past the 10-photo cap) is reported in\n`warnings` rather than discarding the rest of the submission — the valid\nphotos, the signature and the note are already saved.\n\n**Outcome.** Work whose campaign does not require review moves straight to\n`done`. Work on a `requires_review` campaign moves to `submitted` and waits\nfor a reviewer (see the accept/reject endpoints); the response `item.status`\nsays which happened.\n\n**Refused (`422`).** `proof_required` (a required photo/signature is still\nmissing), `already_done` / `missed` / `in_review` (the work is already in a\nterminal or in-review state), or a completion-policy code the shared gate\nraises (`off_shift`, `off_site`, `location_required`, `not_assigned` are\nreturned as `403`; other policy codes as `422`).\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The campaign item (the obligation) to mark done."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "photos": {
                    "type": "array",
                    "description": "Photo proof (JPG, PNG, GIF, WEBP or HEIC; up to 10 files, each ≤ 10 MB). Files past those limits are reported in `warnings`, not fatal.",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    }
                  },
                  "signature_data": {
                    "type": "string",
                    "description": "A signature as a data URL (`data:image/png;base64,…` or `data:image/jpeg;base64,…`), e.g. from a canvas signature pad."
                  },
                  "completion_note": {
                    "type": "string",
                    "description": "Optional note stored on the work's completion-notes trail (≤ 500 chars).",
                    "example": "Endcap reset and faced; dated the stock."
                  },
                  "latitude": {
                    "type": "string",
                    "description": "Optional GPS latitude, for a geofenced campaign's site check."
                  },
                  "longitude": {
                    "type": "string",
                    "description": "Optional GPS longitude, for a geofenced campaign's site check."
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "signature_data": {
                    "type": "string",
                    "description": "A signature data URL (when no photo upload is needed)."
                  },
                  "completion_note": {
                    "type": "string",
                    "description": "Optional note stored on the work's completion-notes trail (≤ 500 chars).",
                    "example": "Endcap reset and faced; dated the stock."
                  },
                  "latitude": {
                    "type": "string"
                  },
                  "longitude": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The work was marked done (or submitted for review). Returns the updated item, plus `warnings` when part of the submitted proof was refused.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "item": {
                      "$ref": "#/components/schemas/CompleteItem"
                    },
                    "warnings": {
                      "type": "array",
                      "description": "Present only when part of the proof was refused (e.g. a non-image file). Human sentences, one per refused item.",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "notes.txt isn't an image. Use a JPG, PNG, GIF, WEBP, or HEIC."
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the token lacks a write scope (`insufficient_permissions`), the My Day surface is off (`surface_disabled`), the caller isn't the holder/admin (`forbidden` — \"This work isn't assigned to you.\"), or a completion policy stop (`off_shift`, `off_site`, `location_required`, `not_assigned`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompleteError"
                }
              }
            }
          },
          "404": {
            "description": "No such item in this business (`not_found`)."
          },
          "422": {
            "description": "The work can't be completed: `proof_required` (a required photo/signature is missing), `already_done`, `missed`, `in_review`, or another completion-policy refusal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompleteError"
                }
              }
            }
          }
        }
      }
    },
    "/frontline_execution/items/{id}/checklist/{checklist_item_id}": {
      "patch": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Mark a checklist step done (or un-tick it)",
        "description": "Ticks one checklist step on the obligation's minted checklist Task. Send\n`completed=false` to un-tick. Returns the freshly-read checklist (the same\nshape `GET /frontline_execution/items/{id}` renders under `checklist`), the\nupdated obligation, whether **every** step is now done (`all_completed` — the\ncue to offer the obligation's Complete action), and the minted Task's status.\n\nSteps authored inline in this app carry no evidence requirements. A campaign\npointed at a Tasks-app template can flag a step \"notes required\" or \"photo\nrequired\"; supply `notes` and/or a multipart `photo` when it does. A step\nthat already carries notes/photos is accepted without re-supplying them.\n\nTicking an already-ticked step (or un-ticking an open one) is a **success**\nno-op — the desired state already holds. When the tick lands but the parent\nTask cannot auto-complete (a missing completion requirement), the step is\nstill ticked and the reason is returned under `warnings`.\n\nRefused (`422`): `no_checklist` (the work is not a checklist Task with steps\n— an inspection, a simple task, or an unclaimed obligation), `task_finished`\n(the Task is already completed/cancelled — reopen it first), `requires_notes`\n/ `requires_photo` (the step demands evidence none was supplied for), or\n`save_failed` (the row would not save). `404` `not_found` for an unknown step.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The obligation (CampaignItem) whose minted work holds the step.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "checklist_item_id",
            "in": "path",
            "required": true,
            "description": "The checklist step (TaskChecklistItem) to tick.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "completed": {
                    "type": "boolean",
                    "default": true,
                    "description": "Tick the step (default) or, when false, un-tick it.",
                    "example": true
                  },
                  "notes": {
                    "type": "string",
                    "nullable": true,
                    "description": "Completion notes — required only for a step flagged \"notes required\".",
                    "example": "Cooler at 3°C, logged on the sheet."
                  }
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "completed": {
                    "type": "boolean",
                    "default": true
                  },
                  "notes": {
                    "type": "string",
                    "nullable": true
                  },
                  "photo": {
                    "type": "string",
                    "format": "binary",
                    "description": "A single image (PNG/JPG/GIF/WebP/HEIC/HEIF/AVIF, ≤10MB) — required only for a step flagged \"photo required\"."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The step was updated. Returns the refreshed checklist and obligation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrontlineChecklistItemResult"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the token lacks a write scope (`insufficient_permissions`), the My Day surface is off (`surface_disabled`), or the caller is not the holder of this work and not an admin (`forbidden`)."
          },
          "404": {
            "description": "No such obligation in this business, or no such step on its work (`not_found`)."
          },
          "422": {
            "description": "The tick was refused: `no_checklist`, `task_finished`, `requires_notes`, `requires_photo`, `photo_failed` or `save_failed`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrontlineChecklistItemError"
                }
              }
            }
          }
        }
      }
    },
    "/frontline_execution/items/{id}/accept": {
      "post": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Accept (approve) a submitted item",
        "description": "Approves the submission: the work moves to `done`, the caller is stamped\nas the reviewer, and the optional `note` is stored as the review note.\n\nRefused (`422`) when the work is not awaiting review (`not_submitted` —\nincluding losing a race to another reviewer), or when the caller submitted\nthe work themselves and separation of duties bars self-review\n(`self_review`).\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The campaign item (submitted work) to accept."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "note": {
                    "type": "string",
                    "description": "Optional note stored with the approval.",
                    "example": "Approved — matches the planogram."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted. Returns the updated item.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "item": {
                      "$ref": "#/components/schemas/ReviewDecisionItem"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the token lacks `write:execution` (`insufficient_permissions`), the My Day surface is off (`surface_disabled`), or the caller can't review work at this location (`forbidden`)."
          },
          "404": {
            "description": "No such item in this business (`not_found`)."
          },
          "422": {
            "description": "The work can't be accepted: `not_submitted` (not awaiting review, or a concurrent reviewer decided first) or `self_review` (the reviewer did the work).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReviewDecisionError"
                }
              }
            }
          }
        }
      }
    },
    "/frontline_execution/items/{id}/reject": {
      "post": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Reject (send back for rework) a submitted item",
        "description": "Sends the submission back: the work moves to `reopened` and the `note` is\nstored as the review note and sent to the worker.\n\nThe `note` (the reason) is **required** — it is the only thing the person\nwho did the work sees, so a reason-free send-back leaves them nothing to\nfix. A blank or too-short reason is refused with `reason_required`, and the\nwork stays in review.\n\nAlso refused (`422`) when the work is not awaiting review (`not_submitted`)\nor the caller submitted it themselves (`self_review`).\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The campaign item (submitted work) to send back."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "note"
                ],
                "properties": {
                  "note": {
                    "type": "string",
                    "description": "The reason for the send-back (required). It is shown to the worker on their My Day card and in the notification.",
                    "example": "Reshoot the left bay — it's out of focus."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sent back for rework. Returns the updated item.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "item": {
                      "$ref": "#/components/schemas/ReviewDecisionItem"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the token lacks `write:execution` (`insufficient_permissions`), the My Day surface is off (`surface_disabled`), or the caller can't review work at this location (`forbidden`)."
          },
          "404": {
            "description": "No such item in this business (`not_found`)."
          },
          "422": {
            "description": "The work can't be rejected: `reason_required` (no/blank reason), `not_submitted` (not awaiting review, or a concurrent reviewer decided first) or `self_review` (the reviewer did the work).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReviewDecisionError"
                }
              }
            }
          }
        }
      }
    },
    "/frontline_execution/items/{id}/block": {
      "post": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Flag an obligation as blocked (\"I can't do this\")",
        "description": "Records that the caller cannot do this work, with a structured `reason`\nand an optional free-text `note`, and notifies the location's accountable\nmanager. Returns the item with its `blocked` state so a client can confirm\nthe flag took.\n\nThe `blocked_reason` is **required** and must be one of the allowed\ncauses — a flag with no cause tells the manager nothing. A blank or\nunknown reason is refused with `invalid_reason`.\n\nA byte-identical re-flag (same reason and note on already-blocked work) is\nabsorbed as a no-op success — it does not re-notify. Changing the reason\nor the note is a legitimate correction and goes through, records, and\nre-notifies.\n\nRefused (`422`) when the work is already finished (`already_done`) or is\nwaiting on a reviewer (`in_review`).\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The obligation (CampaignItem) to flag.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "blocked_reason"
                ],
                "properties": {
                  "blocked_reason": {
                    "type": "string",
                    "description": "What is blocking this work (required). One of the fixed set of causes, so the manager and the analytics rollup can tell them apart.",
                    "enum": [
                      "no_stock",
                      "missing_fixture_or_supplies",
                      "equipment_down",
                      "not_enough_time",
                      "unclear_instructions",
                      "other"
                    ],
                    "example": "no_stock"
                  },
                  "blocked_note": {
                    "type": "string",
                    "description": "Optional free-text detail for the manager (e.g. \"Truck is late — expected 2pm\"). Capped at 500 characters (truncated, not refused).",
                    "example": "Fridge 2 alarm is sounding, panel reads ERR. Called facilities at 9:20."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Flagged (or the identical re-flag absorbed). Returns the item with its blocked state.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "item": {
                      "$ref": "#/components/schemas/FrontlineBlockedItem"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the token lacks a write scope (`insufficient_permissions`), the My Day surface is off (`surface_disabled`), or a plain member may not flag this work (`forbidden` — not the holder, and not on the roster for a must-read)."
          },
          "404": {
            "description": "No such obligation in this business (`not_found`)."
          },
          "422": {
            "description": "The flag was refused: `invalid_reason` (missing/blank/unknown reason), `not_assigned` (a non-holder — even an admin — flagging assigned work, or a non-roster caller flagging a must-read), `already_done` (finished work) or `in_review` (waiting on a reviewer).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrontlineBlockError"
                }
              }
            }
          }
        }
      }
    },
    "/frontline_execution/campaigns/{campaign_id}/questions": {
      "get": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Read a campaign's Q&A thread",
        "description": "Returns the campaign's questions, each with its replies, newest questions\nfirst (re-ordered to read top-to-bottom). The thread is the most recent\nslice; `meta.truncated` is true when there are more questions than the\nslice, and `question_count` is the true total (top-level questions) so a\nclient can render the \"N asked\" counter.\n\nReadable by the author tier or anyone who holds the campaign's work,\nregardless of the campaign's status — a closed campaign's answers stay\nlegible to every store that worked it.\n",
        "parameters": [
          {
            "name": "campaign_id",
            "in": "path",
            "required": true,
            "description": "The campaign whose Q&A thread to read.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The Q&A thread.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrontlineQuestionThread"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the token lacks a read scope (`insufficient_permissions`), the Campaigns surface is off (`surface_disabled`), or the caller may not see this campaign's questions (`forbidden` — not the author tier and not a holder)."
          },
          "404": {
            "description": "No such campaign in this business (`not_found`)."
          }
        }
      },
      "post": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Ask a question on a campaign (or reply to one)",
        "description": "Posts a new question, or — with `parent_comment_id` — a reply aimed at an\nexisting question, and notifies HQ (the campaign's author and everyone\nalready in the thread, capped and deduped). Returns the created comment,\nhow many were `notified`, a human `message` to flash, and the updated\n`question_count`.\n\nThe `body` is **required** and capped at 2000 characters. A blank body is\nrefused with `blank`; an over-length body with `too_long`; a\n`parent_comment_id` that does not resolve to one of this campaign's own\ntop-level questions with `invalid_parent` (404). A closed campaign refuses\na new question with `forbidden`.\n",
        "parameters": [
          {
            "name": "campaign_id",
            "in": "path",
            "required": true,
            "description": "The campaign to ask on.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "properties": {
                  "body": {
                    "type": "string",
                    "maxLength": 2000,
                    "description": "The question (or reply) text. May contain @mentions.",
                    "example": "Do the small-format stores skip the endcap, or is it required everywhere?"
                  },
                  "parent_comment_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Omit to ask a NEW question. Set to a top-level question's id on this campaign to REPLY to it (replies go one level deep).",
                    "example": 512
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Posted. Returns the created comment, the notified count and the new question total.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "question": {
                      "$ref": "#/components/schemas/FrontlineQuestion"
                    },
                    "notified": {
                      "type": "integer",
                      "description": "How many people were actually notified (0 when there is no one but the poster).",
                      "example": 1
                    },
                    "message": {
                      "type": "string",
                      "description": "A human notice describing what happened.",
                      "example": "Question posted — HQ is notified."
                    },
                    "question_count": {
                      "type": "integer",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the token lacks a write scope (`insufficient_permissions`), the Campaigns surface is off (`surface_disabled`), or the caller may not ask on this campaign (`forbidden` — not the author tier and not a holder, or the campaign is closed)."
          },
          "404": {
            "description": "No such campaign in this business (`not_found`), or a `parent_comment_id` that does not resolve to one of this campaign's top-level questions (`invalid_parent`)."
          },
          "422": {
            "description": "The body was refused: `blank` (empty) or `too_long` (over 2000 characters).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrontlineQuestionError"
                }
              }
            }
          }
        }
      }
    },
    "/frontline_execution/items/{id}/release": {
      "post": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Release (give back) an assigned item",
        "description": "Detaches the holder from the work and returns it to the pool: the item\nmoves to `open`, its `current_assignee` is cleared, and a `release` event\nis written to the item's history naming the person who held it (so \"who\nheld it\" reads the same in the history even when an admin does the\nreleasing). Any open \"this shouldn't be mine\" challenge on the item is\nwithdrawn — releasing answers it. The obligation, its due date and its\nlocation are unchanged.\n\nOnly the HOLDER of the item, or an admin, may release it. A caller who is\nneither is refused `403` before anything is written.\n\nRefused (`422`) when the work is already finished (`already_done`), is\nwaiting on a reviewer (`in_review`), or is not currently assigned to\nanyone (`not_assigned` — there is nothing to release).\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The campaign item (assigned work) to release."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "description": "Optional note stored on the release event (why the work was handed back). Unlike a manager's return-to-pool, the holder's own release does not require a reason.",
                    "example": "My shift is ending — someone on the next shift can pick this up."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Released. Returns the updated item (now `open`, no assignee).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "item": {
                      "$ref": "#/components/schemas/ReleasedItem"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the token lacks a write scope (`insufficient_permissions`), the My Day surface is off (`surface_disabled`), or the caller does not hold the work and is not an admin (`forbidden` — \"This work isn't assigned to you.\")."
          },
          "404": {
            "description": "No such item in this business (`not_found`)."
          },
          "422": {
            "description": "The work can't be released: `already_done` (settled), `in_review` (waiting on a reviewer) or `not_assigned` (nobody holds it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReleaseError"
                }
              }
            }
          }
        }
      }
    },
    "/frontline_execution/items/{id}/note": {
      "post": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Add a note to an obligation's work record",
        "description": "Appends a free-text `note` to the work's shared completion-notes trail,\nrecords a `note_added` activity, and notifies the location's reviewer.\nReturns the item plus the note that was recorded (author, body, timestamp)\nso a client can append it to the thread without a second round-trip.\n\nThe `note` is **required**. A blank or non-scalar value is refused with\n`invalid_note` — nothing is written.\n\nRefused (`422`) when the obligation cannot take a note: `acknowledgement`\n(a must-read attestation mints no work record — confirm the read instead),\n`unsupported_work` (an inspection, whose notes live beside its answers in\nInspections), or `no_work` (the work record could not be minted yet — a\ntransient state, retrying may help).\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The obligation (CampaignItem) to note.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "note"
                ],
                "properties": {
                  "note": {
                    "type": "string",
                    "description": "The note text (required). Capped at 500 characters (truncated, not refused).",
                    "example": "Endcap reset done, but two SKUs were out of stock — flagged to the DM."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The note was recorded. Returns the item and the recorded note.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "item": {
                      "$ref": "#/components/schemas/FrontlineNoteItem"
                    },
                    "note": {
                      "$ref": "#/components/schemas/FrontlineRecordedNote"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the token lacks a write scope (`insufficient_permissions`), the My Day surface is off (`surface_disabled`), or the caller is not the holder of this work and not an admin (`forbidden`)."
          },
          "404": {
            "description": "No such obligation in this business (`not_found`)."
          },
          "422": {
            "description": "The note was refused: `invalid_note` (missing / blank / non-scalar), `acknowledgement` (a must-read obligation with no work record), `unsupported_work` (an inspection) or `no_work` (work not mintable yet).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrontlineNoteError"
                }
              }
            }
          }
        }
      }
    },
    "/frontline_execution/items/{id}/nudge": {
      "post": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Nudge the holder of one obligation (a manager reminder)",
        "description": "Sends a reminder to whoever still owes this obligation — its current\nholder, or the location's accountable manager when it is unclaimed.\n\nThe response always names the `recipient` and reports whether the reminder\nwas actually delivered:\n  * `nudged: true` — a fresh reminder was sent (in-app + push).\n  * `nudged: false`, `throttled: true` — nothing new was sent: the\n    recipient was already reminded within the last 24 hours, or could not\n    be reached (a delivery-preference block, or the app is not accessible\n    to them). The same honest hedge the web Coverage notice makes.\n\n`note` is optional free text that replaces the default reminder body.\n\nRefused (`422`, `no_recipient`) when nobody holds the work and no manager\ncovers its location — there is nobody to nudge.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The obligation (Execution::CampaignItem) whose holder to nudge."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "note": {
                    "type": "string",
                    "description": "Optional message that replaces the default reminder body (\"You still have outstanding work at <store>.\").",
                    "example": "The truck is here — please finish the reset before close."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The nudge was processed. `nudged`/`throttled` say whether a reminder was actually delivered; `recipient` names who it addressed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ItemNudgeResult"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the token lacks `write:execution` (`insufficient_permissions`), the Coverage surface is off (`surface_disabled`), the caller is not in the campaign-author tier (`forbidden`), or the item's store is outside the caller's location span (`forbidden`)."
          },
          "404": {
            "description": "No such item in this business (`not_found`)."
          },
          "422": {
            "description": "Nobody holds the work and no manager covers its location, so there is nobody to nudge (`no_recipient`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ItemNudgeError"
                }
              }
            }
          }
        }
      }
    },
    "/frontline_execution/items/{id}/reassign": {
      "post": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Reassign one obligation to a different person (a manager hand-off)",
        "description": "Hands this obligation to a different person. The work, its deadline and its\ncoverage number stay the same — only the holder changes.\n\nThe target (`user_id`) must be an active member of the business who is\nalready assigned to the item's store. A manager override is deliberately NOT\nheld to the campaign's expected-role match (covering with whoever is on shift\nis the manager's call), but the target must at least work at that location —\nthe same fence the claim pool enforces.\n\n`reason` is REQUIRED (at least 3 characters) — a hand-off is an override of\nthe resolver's pick, and the reason lands on the work's history and is the\none thing the previous holder reads.\n\nAn unclaimed pool item can be reassigned too (the web's \"Assign someone\"):\nit simply becomes claimed by the named person.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The obligation (Execution::CampaignItem) to reassign."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "user_id",
                  "reason"
                ],
                "properties": {
                  "user_id": {
                    "type": "integer",
                    "description": "The person to hand the work to — an active member of the business assigned to the item's store.",
                    "example": 4821
                  },
                  "reason": {
                    "type": "string",
                    "minLength": 3,
                    "description": "Why the work is being handed off (required). Recorded on the work's assignment history and shown to the previous holder.",
                    "example": "Priya went home sick — please finish the reset."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The obligation was reassigned. The body carries the reloaded item, now held by the new person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ItemReassignResult"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the token lacks `write:execution` (`insufficient_permissions`), the My Day surface is off (`surface_disabled`), the caller is not in the reviewer tier, or the item's store is outside the caller's location span (`forbidden`)."
          },
          "404": {
            "description": "No such item in this business (`not_found`)."
          },
          "422": {
            "description": "No `user_id` given (`invalid_request`); the target doesn't work at the item's store (`not_at_location`); the reason is blank or shorter than 3 characters (`reason_required`); the target already holds the work (`already_assigned`); the work is finished (`already_done`) or waiting on a reviewer (`in_review`); or the target has an approved absence over the work's window (a leave refusal).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ItemReassignError"
                }
              }
            }
          }
        }
      }
    },
    "/frontline_execution/items/{id}/reassign_candidates": {
      "get": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "The store roster for reassigning one obligation (with search + pagination)",
        "description": "Returns the people the obligation can be reassigned to — the active members\nassigned to the item's store, ranked most-available first (on shift, then\nleast loaded, then name). Each row carries the availability signals the web\nreassign dropdown shows, and flags the current holder.\n\n`q` narrows the roster by name (first name, last name, or the two joined).\n`page` and `limit` page the ranked result. The base roster is capped at 100\npeople before ranking (the web renders every one as an option); when a store\nhas more, `meta.roster_capped` is true and `q` is how to reach the rest.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The obligation (Execution::CampaignItem) being reassigned."
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Narrow the roster by name (case-insensitive substring).",
            "example": "priya"
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "description": "Page number (default 1)."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            },
            "description": "Rows per page (1–100, default 25)."
          }
        ],
        "responses": {
          "200": {
            "description": "The ranked store roster for this obligation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReassignCandidatesResult"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the token lacks `read:execution` (`insufficient_permissions`), the My Day surface is off (`surface_disabled`), the caller is not in the reviewer tier, or the item's store is outside the caller's location span (`forbidden`)."
          },
          "404": {
            "description": "No such item in this business (`not_found`)."
          }
        }
      }
    },
    "/frontline_execution/coverage": {
      "get": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "The persona-scoped Coverage rollup (nodes / campaign lens)",
        "description": "Returns the rollup for wherever the caller's span sits, in one of two\nlenses. `node` drills into a child location; `view` picks the lens;\n`status` filters the campaign lens.\n",
        "parameters": [
          {
            "name": "node",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "The child location to drill into (a region → its districts, a district → its stores). Omitted, the response opens at the top of the caller's span. A node outside the span returns empty nodes rather than leaking."
          },
          {
            "name": "view",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "nodes",
                "campaign"
              ]
            },
            "description": "The lens. Omitted, the server picks the span's natural default (returned in `default_view`). `nodes` is coerced to `campaign` when the viewer has no child locations to descend into."
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "closed",
                "all"
              ],
              "default": "active"
            },
            "description": "Campaign-lens status filter. `active` (default) is the live work; `closed` the finished programmes; `all` both. Scheduled campaigns never appear — they have no minted work to roll up."
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            },
            "description": "Page of the campaign lens (25 per page)."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            },
            "description": "Campaign-lens page size (clamped 1–100)."
          }
        ],
        "responses": {
          "200": {
            "description": "The rollup for the caller's current node and lens.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CoverageDashboard"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the token lacks `read:execution` (`insufficient_permissions`), the Coverage surface is off (`surface_disabled`), or the caller is not in the campaign-author tier (`forbidden`)."
          }
        }
      }
    },
    "/frontline_execution/coverage/nudge": {
      "post": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Nudge a district (or a campaign's lagging districts) — the rollup, not a task",
        "description": "Sends the rollup to the accountable manager(s) of a lagging node. Pass\n`node` for one district, or `campaign_id` for a campaign's lagging children.\n",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "node": {
                    "type": "integer",
                    "description": "The location (district) to nudge — its accountable manager receives the rollup.",
                    "example": 42
                  },
                  "campaign_id": {
                    "type": "integer",
                    "description": "Nudge the managers of the child nodes lagging on this campaign (below 90%). Combine with `node` to narrow the scope.",
                    "example": 17
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The pass ran (including the all-nudged-today case, which reports nudged_count 0).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CoverageNudgeResult"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible, the token lacks `write:execution`, the Coverage surface is off (`surface_disabled`), or the caller is not in the campaign-author tier."
          },
          "404": {
            "description": "A `campaign_id` that isn't this tenant's (`not_found`)."
          },
          "422": {
            "description": "Neither a node nor a campaign was given (`nothing_to_nudge`), or nothing is below the lag threshold (`nothing_behind`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CoverageNudgeError"
                }
              }
            }
          }
        }
      }
    },
    "/frontline_execution/campaigns/{id}/coverage/remind": {
      "post": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Remind everyone still outstanding on one campaign (span-bounded)",
        "description": "Sends a reminder to every person still holding outstanding work on this\ncampaign inside the caller's span. Idempotent within 24h per person\n(throttled), and capped at 200 deliveries per call.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The campaign to remind on."
          }
        ],
        "responses": {
          "200": {
            "description": "The pass ran (including the empty-roster case, which is a clean 200 saying nobody is outstanding).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CoverageRemindResult"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible, the token lacks `write:execution` (`insufficient_permissions`), the Coverage surface is off (`surface_disabled`), or the caller is not in the campaign-author tier."
          },
          "404": {
            "description": "No such campaign in this business (`not_found`)."
          }
        }
      }
    },
    "/frontline_execution/campaigns/held": {
      "get": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List campaigns held in the publication release gate",
        "description": "Returns the scheduled campaigns awaiting publication approval, newest\nsubmission last (the gate is a queue). Capped and unpaginated — held work\nis rare. Each row carries who submitted it and when; the full release\nspec and whether the caller may decide it come from\n`GET /campaigns/{id}/release`.\n",
        "responses": {
          "200": {
            "description": "The held list (possibly empty).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HeldCampaignsResult"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible, the token lacks `read:execution` (`insufficient_permissions`), the Campaigns surface is off (`surface_disabled`), or the caller is not in the campaign-author tier."
          }
        }
      }
    },
    "/frontline_execution/campaigns/{id}/release": {
      "get": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "The release-review spec for one held/scheduled campaign",
        "description": "Composes the release-review payload for a draft or scheduled campaign.\n`sites`/`obligations` are the projected target size (one obligation per\nresolved location); `same_day` lists the other campaigns landing on the\nsame start date and the total obligations that day. `approval.can_decide`\nis the approval engine's own authority check for the caller (false for the\nsubmitter — separation of duties — and for anyone outside the approver set\nor span).\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The campaign to review."
          }
        ],
        "responses": {
          "200": {
            "description": "The release-review spec.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReleaseReview"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible, the token lacks `read:execution`, the Campaigns surface is off, or the caller is not in the author tier."
          },
          "404": {
            "description": "No such campaign in this business (`not_found`)."
          },
          "422": {
            "description": "The campaign has already launched — nothing to release (`not_releasable`)."
          }
        }
      },
      "post": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Release a held campaign (approve its publication)",
        "description": "Approves the open publication request so the release gate opens. The\ncampaign keeps its scheduled status and launches on its own date — this\nnever launches it directly.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The held campaign to release."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "comment": {
                    "type": "string",
                    "description": "Optional note recorded on the approval action."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Released — the publication request is approved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicationDecisionResult"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible, the token lacks `write:execution`, the Campaigns surface is off, the caller is not in the author tier, or the caller may not decide THIS request (`not_authorized_to_approve` — e.g. they submitted it, or it's outside their span)."
          },
          "404": {
            "description": "No such campaign in this business (`not_found`)."
          },
          "409": {
            "description": "The campaign isn't awaiting release approval (`not_under_review`) or has already launched (`already_launched`)."
          }
        }
      }
    },
    "/frontline_execution/campaigns/{id}/hold": {
      "post": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Hold a campaign (reject its publication)",
        "description": "Rejects the open publication request. A scheduled campaign is returned to\ndraft so it won't auto-launch, and its author is notified with the reason.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The held campaign to hold."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "description": "Optional reason surfaced to the author."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Held — the publication request is rejected and the campaign returned to draft.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicationDecisionResult"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "Same fences as release, including `not_authorized_to_approve` when the caller may not decide this request."
          },
          "404": {
            "description": "No such campaign in this business (`not_found`)."
          },
          "409": {
            "description": "Not awaiting approval (`not_under_review`) or already launched (`already_launched`)."
          }
        }
      }
    },
    "/live_boards/boards": {
      "get": {
        "tags": [
          "Live Boards"
        ],
        "summary": "List boards",
        "description": "Boards visible to the caller.\n\n**Authoring tier** gets every board in the tenant. `status` selects\n`active` (default), `archived`, or `all`, and each row carries every\nactive reading on the board.\n\n**Members** get only boards the audience gate admits them to — a board\nis included when AT LEAST ONE reading on it is readable — and each row\ncarries only the readings they are admitted to. `visualizations_count`\nis that admitted count, never the board's true total, so the number a\nmember is shown always matches the list beside it. `status` is ignored\nfor members: an archived board renders nowhere.\n\nA member's list resolves its audience gate in Ruby and therefore scans\na bounded number of boards. `meta.scan_truncated` reports whether that\nbound was reached — it is present on the member response only.\n\nReadings do NOT carry their data here (one source query per reading\nwould make a page of boards expensive); use the detail endpoint.\n",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Authoring tier only. One of `active` (default), `archived`, `all`.",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "archived",
                "all"
              ]
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Case-insensitive board-name search.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Boards the caller may see",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LiveBoard"
                      }
                    },
                    "total_count": {
                      "type": "integer"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "total_count": {
                          "type": "integer"
                        },
                        "total_pages": {
                          "type": "integer"
                        },
                        "current_page": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        },
                        "has_next_page": {
                          "type": "boolean"
                        },
                        "has_prev_page": {
                          "type": "boolean"
                        },
                        "scan_truncated": {
                          "type": "boolean",
                          "description": "Member responses only. True when the audience scan hit its bound, i.e. the tenant holds more boards than were examined."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Live Boards is not enabled for the business, or not published to users"
          }
        }
      }
    },
    "/live_boards/boards/{id}": {
      "get": {
        "tags": [
          "Live Boards"
        ],
        "summary": "Get a board with its current readings",
        "description": "The board plus every reading the caller is admitted to, each with its\ncurrent data.\n\nReturns **404** — not 403 — for a member admitted to no reading on the\nboard. Which of \"this board is empty\" and \"you are not admitted to it\"\napplies is itself information about a board the caller may not see. The\nauthoring tier is exempt and may open any board.\n\nEach reading's `data` object is shaped by its visualization type\n(`ranked_list`, `stat`, `line`, `table`, `chart`, `movers`, `gauge`,\n`status`, `share`, `calendar`, `streak`, `pace`, `scorecard`); the\ncommon members are documented on `LiveBoardReadingData`. A reading whose\nsource is unavailable returns `{ \"error\": \"...\" }` in place of its data\nrather than failing the whole board.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The board and its readable readings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "board": {
                      "$ref": "#/components/schemas/LiveBoard"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such board in this business, or the caller is admitted to nothing on it"
          },
          "403": {
            "description": "Live Boards is not enabled for the business, or not published to users"
          }
        }
      }
    },
    "/live_boards/playlists": {
      "get": {
        "tags": [
          "Live Boards"
        ],
        "summary": "List rotations (playlists)",
        "description": "Named, ordered board rotations — what a break-room screen plays.\n\n**Authoring tier only** (contributor or admin); members get 403. A\nrotation is authoring material, matching the web, where the Playlists\ntab is contributor-gated.\n\n`rotation_url` and `embed_allowed_origins` are returned **to app admins\nonly**. The rotation URL is an unauthenticated credential: anyone\nholding it reads the rotation with no sign-in, so every control over it\nis admin-tier on every surface. The raw token is never serialized on its\nown.\n",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "archived",
                "all"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Rotations in this business",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LiveBoardPlaylist"
                      }
                    },
                    "total_count": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The caller is not a Live Boards contributor or admin"
          }
        }
      }
    },
    "/live_boards/playlists/{id}": {
      "get": {
        "tags": [
          "Live Boards"
        ],
        "summary": "Get a rotation with its boards",
        "description": "As the list endpoint, plus `boards` — the rotation's readings resolved\nin play order and re-checked against the audience gate. Board ids are\nfrozen at authoring time, so a reading that has since been archived or\nrestricted is omitted rather than named.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The rotation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "playlist": {
                      "$ref": "#/components/schemas/LiveBoardPlaylist"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such rotation in this business"
          },
          "403": {
            "description": "The caller is not a Live Boards contributor or admin"
          }
        }
      }
    },
    "/surveys/surveys": {
      "get": {
        "tags": [
          "Surveys"
        ],
        "summary": "List surveys",
        "description": "Two lists behind one route, selected by `scope`.\n\n**Employee self-service (default, `scope` omitted).** Surveys the caller\nis assigned to, currently open, and has not yet completed — the set\n`Survey.available_for_user` produces minus the ones they have already\nanswered. Ordered by `closes_at` ascending (nulls last), then newest\nfirst. Rows carry no `response_count`.\n\n**`scope=created`.** The authoring list: surveys the caller may manage\n(their own plus any explicitly shared with them; every survey in the\ntenant for an admin), in ALL statuses, newest first. Reachability is the\ndesktop authoring gate verbatim — the tenant's \"who can create and\nmanage surveys\" capability, OR an explicit collaborator grant. Bare\nauthorship is deliberately NOT enough. Rows carry `response_count`.\nA create-capable user with no surveys yet gets an empty list, not a 403.\n\nBoth branches accept `status`, `survey_type` and `search`.\n\n**Unusable parameters are REFUSED, never silently ignored** — an API\ncaller has a machine-readable channel, so a filter this endpoint cannot\nhonour is a `400` naming the accepted set rather than a `200` whose body\nquietly ignored it. `scope`, `status` and `survey_type` are allowlisted\nagainst their enums; `status`, `survey_type` and `search` must each be a\nsingle value (a list- or object-shaped value is a `400`, checked against\nthe raw query string).\n\n`page` is clamped to a ceiling of 1,000,000 before it reaches the\npaginator, so an out-of-range page number cannot 500 the request.\n\nUse this to list my surveys, show surveys assigned to me, see open\nsurveys, check pending surveys, or list the surveys I manage.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "description": "Omit for the caller's own assigned-and-open surveys. `created` switches to the authoring list. Any other value — including a different case, or a list-shaped value — is a 400 `invalid_scope`.",
            "schema": {
              "type": "string",
              "enum": [
                "created"
              ]
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Narrow to one lifecycle status. Mostly useful with `scope=created`; the employee list is already open-surveys-only.",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "active",
                "closed",
                "archived"
              ]
            }
          },
          {
            "name": "survey_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "engagement",
                "pulse",
                "custom",
                "manager_feedback"
              ]
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Case-insensitive match on the survey's name and description only (never the creator's name — `survey_json` returns no creator, so matching on one would let a caller probe user names through a field the response does not contain). Whitespace-tokenised; tokens are ANDed; at most the first 12 words are used, and a longer search is truncated rather than refused.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The surveys the caller may see for the requested scope",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SurveySummary"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/SurveysPaginationMeta"
                    },
                    "total_count": {
                      "type": "integer",
                      "description": "Total rows across all pages (same value as `meta.total_count`)."
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCountRef"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/PlatformHttpMeta"
                    }
                  }
                }
              }
            }
          },
          "304": {
            "description": "The caller sent `If-None-Match` and it matched the ETag in `_meta.http.caching`. Body is empty. Only this endpoint can answer 304 — the other reads here do not render through the piggyback path."
          },
          "400": {
            "description": "An unusable query parameter. `error.code` is one of:\n\n* `invalid_scope` — `scope` is neither absent nor `created`, or is\n  list-shaped. `error.details.accepted_values` names the set.\n* `invalid_status` / `invalid_survey_type` — the value is outside\n  the enum. `error.details.accepted_values` names the set.\n* `invalid_status` / `invalid_survey_type` / `invalid_search` with\n  `error.details.parameter` — that key arrived list- or\n  object-shaped and must be a single value.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "* `feature_not_enabled` — Surveys is not enabled for this business.\n* `forbidden` — the caller does not have access to Surveys (app not\n  published to users, or a visibility rule excludes them).\n* `forbidden` — `scope=created` and the caller neither holds the\n  create-and-manage capability nor an explicit collaborator grant.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/surveys/surveys/{id}": {
      "get": {
        "tags": [
          "Surveys"
        ],
        "summary": "Get a survey with its questions",
        "description": "The survey plus its ordered question list — the payload a native client\nrenders the \"take this survey\" screen from.\n\n**Visibility.** Returned only when the caller may open the definition\n(owner, collaborator, or admin) OR the survey is currently open and the\ncaller is in its audience. A hidden (link-only) survey reached directly\nby id still resolves for someone assigned to it, so onboarding- and\nPM-embedded surveys work. Anything else is `404`, never `403` — which of\n\"no such survey\" and \"not yours\" applies is itself information about a\nsurvey the caller may not see.\n\n**Questions** are the caller-fillable input fields only, in builder\norder; instruction / section / header display blocks are excluded.\n\n**Management numbers are tied to the caller's results TIER for THIS\nsurvey, not to a blanket capability:**\n\n* `:all` / `:aggregate` — `response_count`, `target_audience_count` and\n  `completion_rate` (the whole-survey figures, matching the web).\n* `:team` — `response_count` ONLY, counting the caller's own direct\n  reports, and `null` when an anonymous survey's team count sits below\n  the tenant's anonymity floor. No `target_audience_count`, no\n  `completion_rate` — the web renders no Target or Completion tile for\n  this tier either.\n* `:none` — none of the three fields is present.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The survey and its questions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "survey"
                  ],
                  "properties": {
                    "survey": {
                      "$ref": "#/components/schemas/SurveyDetail"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`feature_not_enabled` (Surveys is not enabled for this business) or `forbidden` (the caller does not have access to the Surveys app).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no such survey in this business, or the caller may neither take nor manage it. Both cases answer the same way on purpose.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/surveys/surveys/{id}/respond": {
      "post": {
        "tags": [
          "Surveys"
        ],
        "summary": "Submit a response to a survey",
        "description": "Records the caller's answers. Re-checks the same guards the in-app\nsubmit path checks, then builds the submission the same way — including\nanonymity handling: `user_id` is always retained for completion\ntracking, `is_anonymous` hides identity on every display surface, and on\nan anonymous survey the stored metadata omits user agent and IP.\n\nAnswers go in `submission_data`, keyed by each question's `field_name`\n(from the detail endpoint's `questions`). Keys the template does not\ndefine are dropped. A payload from which not one recognised key could be\nread is refused with `empty_submission` rather than saved as a blank\nresponse — an accepted blank response would permanently lock the\nrespondent out and inflate everyone's completion rate. A respondent who\nlegitimately leaves every OPTIONAL question blank still posts those\nkeys, so `{\"comments\": \"\"}` is accepted.\n\n**Attachments are not supported on this endpoint and it says so** rather\nthan silently discarding them: this controller runs no upload pipeline,\nso a survey with a required upload field is structurally un-completable\nhere, and a multipart body carrying a file is refused. Complete those in\nthe app or from the survey's own link.\n\nOn success any in-progress draft for this template is cleared, after any\nfiles already attached to that draft are transferred to the new\nsubmission.\n\nOn success this answers **200**, not 201 — the endpoint renders through\n`render_single`, so the body is the single `submission` key with no\npiggyback and no `Location` header.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "submission_data": {
                    "type": "object",
                    "description": "Answers keyed by `field_name`. The permitted shape depends\non the question's `field_type`:\n\n* an ARRAY for `checkbox`, `multiselect`, `multi_choice`,\n  `multiple_choice`, `gallery`, `file`, `image`, `video`,\n  `audio`, `lookup`;\n* an OBJECT for `matrix`, `annotation`, `range`, and for a\n  Likert-configured `scale` (one whose configuration carries\n  both `scale_options` and `statements`);\n* a SCALAR for everything else. A container value on a\n  `rating` / `number` / `slider` / plain `scale` question is\n  dropped by the permit rather than stored.\n\nAn uploaded file is always dropped and then reported — see\n`upload_not_supported` below.\n",
                    "additionalProperties": true
                  }
                },
                "required": [
                  "submission_data"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The response was recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "submission"
                  ],
                  "properties": {
                    "submission": {
                      "$ref": "#/components/schemas/SurveySubmission"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "* `feature_not_enabled` — Surveys is not enabled for this business.\n* `forbidden` — the caller does not have access to the Surveys app.\n* `forbidden` — the caller is not in this survey's audience.\n* `insufficient_scope` — the API token declares scopes but none of\n  them is a write scope, so it may not perform this action.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no such survey in this business.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "409": {
            "description": "`already_completed` — the caller has already responded and the survey's own template does not allow multiple submissions.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "422": {
            "description": "Either a refusal with an `error` object, or model validation\nfailures with an `errors` array. The refusal codes are:\n\n* `survey_not_active` — the survey is not `active`, or the current\n  time is outside its `opens_at`/`closes_at` window.\n* `no_questions` — the survey has no fillable questions.\n* `upload_not_supported` — the survey has a required upload field\n  that this endpoint structurally cannot satisfy\n  (`error.details.required_upload_field_names`), or the request\n  carried an uploaded file\n  (`error.details.rejected_upload_field_names`). Nothing was saved\n  in either case.\n* `empty_submission` — no recognised answer key was found.\n  `error.details.expected_field_names` lists the keys this template\n  reads, in question order.\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "description": "Machine readable error code"
                            },
                            "message": {
                              "type": "string",
                              "description": "Human readable error message"
                            },
                            "details": {
                              "type": "object",
                              "description": "Additional error context"
                            }
                          }
                        }
                      },
                      "required": [
                        "error"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "errors": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "field": {
                                "type": "string",
                                "description": "Field name with validation error"
                              },
                              "message": {
                                "type": "string",
                                "description": "Validation error message"
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/surveys/surveys/{id}/results": {
      "get": {
        "tags": [
          "Surveys"
        ],
        "summary": "Get anonymity-safe results for a survey",
        "description": "Aggregate results only. Raw per-respondent rows and respondent identity\nare never returned by this endpoint, at any tier.\n\n**Who sees what** is `Surveys::AccessPolicy#results_scope` (see the file\nheader). `:all` and `:aggregate` read the org-wide aggregate and report\n`scope: \"full\"`; `:team` reads a slice narrowed to the caller's own\ndirect reports and reports `scope: \"team\"`; `:none` is a `403`.\nConfidential surveys and the per-survey manager opt-in are honoured by\nthe policy itself.\n\n**The anonymity floor** is the tenant's `minimum_response_threshold`,\nresolved through the app's single resolver, which applies a platform\nfloor of 5 that a tenant may raise but not lower. It suppresses in three\nplaces:\n\n1. On an ANONYMOUS survey whose response count (for this scope) is below\n   the threshold, no aggregates at all are returned:\n   `anonymity_protected: true`, `threshold_met: false`, and\n   `question_summaries: []`.\n2. On an ANONYMOUS survey at the `:team` tier below the threshold, the\n   participation count itself is masked to `null` — a manager with one\n   direct report would otherwise learn whether that named person\n   answered.\n3. Per question: a numeric or choice question with fewer answers than\n   the threshold returns its count and `insufficient_data: true` in\n   place of the distribution. Free-text questions return a count of\n   non-blank answers and never the answers themselves, at any volume.\n\nNon-anonymous surveys are not suppressed at the survey level (1 and 2 do\nnot apply), but per-question suppression (3) still does.\n\nThis endpoint is not paginated: the whole result set for the caller's\nscope is rolled up in one pass.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The aggregate results the caller's tier permits",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "results"
                  ],
                  "properties": {
                    "results": {
                      "$ref": "#/components/schemas/SurveyResults"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "* `feature_not_enabled` — Surveys is not enabled for this business.\n* `forbidden` — the caller does not have access to the Surveys app.\n* `forbidden` — the caller's results tier for this survey is\n  `:none`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no such survey in this business.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/surveys/surveys/{id}/remind": {
      "post": {
        "tags": [
          "Surveys"
        ],
        "summary": "Send a reminder to employees who have not responded",
        "description": "Queues `SurveyBulkDeliveryJob` in reminder mode. The job recomputes the\nnon-responder set itself, so nothing is passed in and no recipient list\nis accepted.\n\n**Three gates run before anything is queued.** The caller must hold the\nsurvey's lifecycle tier (owner, co-owner, or admin). The survey must be\nactive. And when the survey targets the ENTIRE company, the tenant's\n`send_to_entire_company` capability must also admit the caller — the\nsame ceiling the web and the agent enforce; a department- or\ngroup-targeted reminder is unaffected by it.\n\n**Then three refusals establish there is somebody to send to**, computed\nexactly as the web twin computes them, so the two doors cannot disagree\nabout who counts as a recipient: an empty target audience, an audience\nwhere everyone has already responded, and an audience whose\nnon-responders all lack an email address (the reminder leg mails only\naddressable users and publishes no in-app item, so those people would\nreceive nothing).\n\n`recipient_count` is what will actually be mailed — non-responders with\nan email address on file — not the raw non-responder count.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Reminder delivery was queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "accepted",
                    "survey_id",
                    "recipient_count",
                    "message"
                  ],
                  "properties": {
                    "accepted": {
                      "type": "boolean",
                      "example": true
                    },
                    "survey_id": {
                      "type": "integer"
                    },
                    "recipient_count": {
                      "type": "integer",
                      "description": "Non-responders with an email address on file — the number that will actually be mailed."
                    },
                    "message": {
                      "type": "string",
                      "example": "Reminder delivery has been queued for 12 employees who haven't responded yet."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "* `feature_not_enabled` — Surveys is not enabled for this business.\n* `forbidden` — the caller does not have access to the Surveys app.\n* `forbidden` — the caller does not hold this survey's lifecycle\n  tier (owner, co-owner, or admin).\n* `forbidden` — the survey targets the entire company and the\n  tenant limits who may send company-wide.\n* `insufficient_scope` — the API token declares scopes but none of\n  them is a write scope.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no such survey in this business.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "422": {
            "description": "Nothing was queued. `error.code` is one of:\n\n* `survey_not_active` — reminders are only sent for an active\n  survey.\n* `empty_audience` — no employees are in the target audience.\n* `all_responded` — every targeted employee has already responded.\n* `no_reachable_recipients` — none of the non-responders has an\n  email address on file.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/surveys/feedback": {
      "post": {
        "tags": [
          "Surveys"
        ],
        "summary": "Submit anonymous feedback",
        "description": "The always-on anonymous feedback channel — the \"speak up\" box that is\nseparate from any particular survey. The submitter's identity is not\nrecorded on the row; what comes back is a one-way `claim_code` they keep\nin order to look up the status of their own submission later.\n\nAvailable only while the tenant has the anonymous feedback channel\nenabled (`anonymous_feedback_enabled`, on by default). Submitting also\nnotifies the tenant's feedback triagers, matching the desktop path.\n\nUse this to submit anonymous feedback, report a concern anonymously,\nraise an issue without giving my name, or speak up.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "feedback"
                ],
                "properties": {
                  "feedback": {
                    "type": "object",
                    "description": "Must be an object. A missing `feedback` key, or one whose value is a scalar or a list, is a 400 `parameter_missing`. Only `topic` and `message` are read; anything else is dropped.",
                    "required": [
                      "message"
                    ],
                    "properties": {
                      "topic": {
                        "type": "string",
                        "enum": [
                          "workload",
                          "safety",
                          "leadership",
                          "culture",
                          "compensation",
                          "communication",
                          "other"
                        ],
                        "default": "other",
                        "description": "Omitting the key defaults it to `other`. An EMPTY STRING is not defaulted and fails validation — send the key absent rather than blank."
                      },
                      "message": {
                        "type": "string",
                        "minLength": 10,
                        "maxLength": 12000
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The feedback was recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "feedback"
                  ],
                  "properties": {
                    "feedback": {
                      "$ref": "#/components/schemas/SurveyAnonymousFeedback"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`parameter_missing` — `feedback` was absent, or present but not an object.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "* `feature_not_enabled` — Surveys is not enabled for this business.\n* `forbidden` — the caller does not have access to the Surveys app.\n* `feature_not_enabled` — the anonymous feedback channel is turned\n  off for this business.\n* `insufficient_scope` — the API token declares scopes but none of\n  them is a write scope.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/templates": {
      "get": {
        "tags": [
          "Workspace"
        ],
        "summary": "List workspace templates",
        "description": "The workspace templates available to the caller: the system-provided\nset plus any the tenant has authored. App-wide, so this is the one\nendpoint in the namespace with no workspace and no membership gate.\n\nEach row carries its full `structure` blueprint (the sections, task\nlists and seed content a workspace created from it receives), so keep\n`per_page` modest.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Rows per page. Maximum 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Alias for `per_page`, kept because it is what these endpoints shipped with. `per_page` wins when both are sent. A blank or non-numeric value falls back to the endpoint's default.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Templates available to the caller",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "templates",
                    "total_count",
                    "meta"
                  ],
                  "properties": {
                    "templates": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WorkspaceTemplate"
                      }
                    },
                    "total_count": {
                      "type": "integer",
                      "description": "Templates matching the request, across all pages."
                    },
                    "meta": {
                      "$ref": "#/components/schemas/WorkspacePaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Workspace app is not enabled for the business, or not accessible to the caller (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/templates/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The template id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Workspace"
        ],
        "summary": "One template",
        "description": "One workspace template, with its full `structure` blueprint.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The template",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "template"
                  ],
                  "properties": {
                    "template": {
                      "$ref": "#/components/schemas/WorkspaceTemplate"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Workspace app is not enabled for the business, or not accessible to the caller (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No active template with that id is visible to this business (error code `not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces": {
      "get": {
        "tags": [
          "Workspace"
        ],
        "summary": "List the caller's workspaces",
        "description": "The workspaces the CALLER belongs to — an explicit membership or the\nworkspace's notification recipient group.\n\nThis is membership-scoped for every caller **including workspace app\nadmins**, matching the web and mobile lists. An admin's list is the\nworkspaces they are part of; any other workspace in the tenant is\nstill reachable by addressing it directly at\n`GET /workspace/workspaces/{id}`.\n\nOrdered most-recently-updated first. Each row's `role` is resolved for\nthe caller: their membership role, `member` for a rule-group member\nwith no explicit row, or `null` when they have no role in it.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "filter",
            "in": "query",
            "required": false,
            "description": "Which workspaces to include. `active` (the default), `archived`, or `all`. An unrecognised value behaves as `active`.",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "archived",
                "all"
              ],
              "default": "active"
            }
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Rows per page. Maximum 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Alias for `per_page`, kept because it is what these endpoints shipped with. `per_page` wins when both are sent. A blank or non-numeric value falls back to the endpoint's default.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's workspaces",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "workspaces",
                    "total_count",
                    "meta"
                  ],
                  "properties": {
                    "workspaces": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Workspace"
                      }
                    },
                    "total_count": {
                      "type": "integer",
                      "description": "Workspaces matching the filter, across all pages."
                    },
                    "meta": {
                      "$ref": "#/components/schemas/WorkspacePaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Workspace app is not enabled for the business, or not accessible to the caller (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Workspace"
        ],
        "summary": "Create a workspace",
        "description": "Creates a workspace and makes the caller its owner.\n\nWho may create is a tenant capability policy (the Workspace app's\n`create_workspace` setting — manager-or-above by default, widenable to\nspecific groups or everyone). A caller the policy excludes gets `422`\nnaming the reason rather than a silent no-op.\n\nThe new workspace has NO task list. `POST .../tasks` creates a default\none on first use, so no extra call is needed.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "The workspace name.",
                    "example": "Acme Q3 Rollout"
                  },
                  "description": {
                    "type": "string",
                    "nullable": true
                  },
                  "icon": {
                    "type": "string",
                    "nullable": true,
                    "description": "Icon key, as shown in the web workspace form."
                  },
                  "color": {
                    "type": "string",
                    "nullable": true,
                    "description": "Colour key, as shown in the web workspace form."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created workspace",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "workspace"
                  ],
                  "properties": {
                    "workspace": {
                      "$ref": "#/components/schemas/Workspace"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Workspace app is not enabled for the business, or not accessible to the caller (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Nothing was created (error code `validation_failed`). Either the attributes are invalid (blank name, name already taken) or the tenant's `create_workspace` policy does not admit the caller. `error.details` carries the individual messages.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The workspace's numeric id OR its slug.",
          "schema": {
            "type": "string"
          },
          "example": "42"
        }
      ],
      "get": {
        "tags": [
          "Workspace"
        ],
        "summary": "One workspace",
        "description": "A single workspace, addressable by numeric id OR slug.\n\nMembers and rule-group members see the workspaces they belong to;\nworkspace app admins reach any workspace in the tenant, matching the\nweb (`authorize_member!` admits app admins) and mobile surfaces.\nAnything else is `404`, never `403`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The workspace",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "workspace"
                  ],
                  "properties": {
                    "workspace": {
                      "$ref": "#/components/schemas/Workspace"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Workspace app is not enabled for the business, or not accessible to the caller (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No workspace with that id or slug is reachable by the caller (error code `not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Workspace"
        ],
        "summary": "Update a workspace",
        "description": "The correction path for create — rename, re-describe, restyle.\n\nOWNER-ONLY (a workspace app admin also qualifies), the same rule the\nweb applies with `authorize_owner!`. The gate lives in the shared\nservice, so this surface, the web and the Ask AI agent cannot drift.\n\n**Only the keys you send are written.** A request carrying just `name`\nleaves the description untouched — it does not blank it.\n\n`PUT` is routed to the same action and behaves identically.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string",
                    "nullable": true
                  },
                  "icon": {
                    "type": "string",
                    "nullable": true
                  },
                  "color": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated workspace",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "workspace"
                  ],
                  "properties": {
                    "workspace": {
                      "$ref": "#/components/schemas/Workspace"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Workspace app is not enabled or not accessible, or the caller is neither the workspace owner nor a workspace app admin (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No workspace with that id or slug is reachable by the caller (error code `not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Nothing was written; the attributes are invalid (error code `validation_failed`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{id}/client-digest": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The workspace's numeric id OR its slug.",
          "schema": {
            "type": "string"
          },
          "example": "42"
        }
      ],
      "get": {
        "tags": [
          "Workspace"
        ],
        "summary": "Generate the client digest",
        "description": "Renders the workspace's client-facing digest as Markdown — the public\nmessage highlights, completed tasks and hill-chart positions a client\nmay see.\n\n**OWNER-ONLY** (a workspace app admin also qualifies). This assembles\nexactly what leaves the company, so membership alone is not enough: a\nread-only viewer must not be able to produce it.\n\nAlso requires the business-level `client_digest_enabled` toggle.\n\nContent marked internal-only anywhere in the workspace is excluded by\nconstruction — the same redaction the web digest and the client portal\napply.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The generated digest",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "workspace_id",
                    "format",
                    "digest"
                  ],
                  "properties": {
                    "workspace_id": {
                      "type": "integer"
                    },
                    "format": {
                      "type": "string",
                      "enum": [
                        "markdown"
                      ]
                    },
                    "digest": {
                      "type": "string",
                      "description": "The digest body, in Markdown."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller is not the workspace owner (nor a workspace app admin), or client digests are disabled for this business (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No workspace with that id or slug is reachable by the caller (error code `not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The digest could not be generated (error code `digest_failed`). The message is deliberately fixed; the underlying detail is logged server-side rather than returned.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{workspace_id}/messages": {
      "parameters": [
        {
          "name": "workspace_id",
          "in": "path",
          "required": true,
          "description": "The workspace's numeric id OR its slug.",
          "schema": {
            "type": "string"
          },
          "example": "42"
        }
      ],
      "get": {
        "tags": [
          "Workspace"
        ],
        "summary": "List message-board posts",
        "description": "The workspace's message board, most recent first. Every row carries a\n`comment_count`, so a caller can tell which threads have replies\nwithout fetching them.\n\nRequires the business-level `enable_message_board` toggle.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Rows per page. Maximum 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Alias for `per_page`, kept because it is what these endpoints shipped with. `per_page` wins when both are sent. A blank or non-numeric value falls back to the endpoint's default.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Message-board posts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "messages",
                    "total_count",
                    "meta"
                  ],
                  "properties": {
                    "messages": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WorkspaceMessage"
                      }
                    },
                    "total_count": {
                      "type": "integer",
                      "description": "Posts on this board, across all pages."
                    },
                    "meta": {
                      "$ref": "#/components/schemas/WorkspacePaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Workspace app is not enabled for the business or not accessible to the caller, the caller is not a member of this workspace, or the canvas section this endpoint serves is switched off for the business (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No workspace with that id or slug is reachable by the caller (error code `not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Workspace"
        ],
        "summary": "Post to the message board",
        "description": "Posts a new message as the caller. CONTRIBUTORS ONLY — a member whose\nrole is `viewer` gets `403`.\n\n`@mentions` in the body are extracted and notified by the same service\nthe web uses.\n\nSend `Idempotency-Key` to make a network retry replay the original\nresponse without posting again. `external_id` is a durable identity:\nsending the same payload and id later returns the existing message;\nreusing it for different content returns `409`.\n\nRequires the business-level `enable_message_board` toggle.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Retry key retained by the platform for 24 hours.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "title",
                  "body"
                ],
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "body": {
                    "type": "string",
                    "description": "Message body. `@mentions` are extracted and notified."
                  },
                  "internal_only": {
                    "type": "boolean",
                    "default": false,
                    "description": "Excludes the post from the client digest and the client portal. Omitted means `false`."
                  },
                  "external_id": {
                    "type": "string",
                    "maxLength": 255,
                    "description": "Optional caller-owned durable identity, unique within this workspace. Use it for resumable publishing jobs."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Existing message replayed for the same `external_id` and identical content.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message",
                    "replayed"
                  ],
                  "properties": {
                    "message": {
                      "$ref": "#/components/schemas/WorkspaceMessage"
                    },
                    "replayed": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "The created message",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message",
                    "replayed"
                  ],
                  "properties": {
                    "message": {
                      "$ref": "#/components/schemas/WorkspaceMessage"
                    },
                    "replayed": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The app or the message board is disabled, the caller is not a member, or the caller is a read-only viewer (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No workspace with that id or slug is reachable by the caller (error code `not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The idempotency key or durable `external_id` was reused for a different request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Nothing was created; the message is invalid (error code `validation_failed`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{workspace_id}/messages/{id}": {
      "parameters": [
        {
          "name": "workspace_id",
          "in": "path",
          "required": true,
          "description": "The workspace's numeric id OR its slug.",
          "schema": {
            "type": "string"
          },
          "example": "42"
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The message id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Workspace"
        ],
        "summary": "One message with its comments",
        "description": "A single post together with a page of its comment thread.\n\nThe thread is CAPPED (`comment_limit`, default 25, maximum 100) —\n`comment_count` on the message tells you whether there is more.\n\nA comment posted through the Workspace Client Portal has no platform\naccount behind it: those carry `author.external: true`, the client's\naddress as `author.email`, and a null `author.id`. This mirrors what\nthe web shows internal readers.\n\nRequires the business-level `enable_message_board` toggle.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "comment_limit",
            "in": "query",
            "required": false,
            "description": "Comments to return with the message. Maximum 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The message and a page of its comments",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message"
                  ],
                  "properties": {
                    "message": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/WorkspaceMessage"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "comments": {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/WorkspaceMessageComment"
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Workspace app is not enabled for the business or not accessible to the caller, the caller is not a member of this workspace, or the canvas section this endpoint serves is switched off for the business (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such workspace or message (error code `not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Workspace"
        ],
        "summary": "Edit a message",
        "description": "The correction path for posting. **Author-only** — or a workspace app\nadmin. A contributor may post their own message but must not rewrite a\ncolleague's, the same rule the web applies.\n\n**Only the keys you send are written**, so a request carrying just\n`title` will not blank the body. Send at least one of `title`, `body`,\n`internal_only`, or the request is refused with `422`.\n\n`@mentions` are re-extracted on edit. `PUT` behaves identically.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Retry key retained by the platform for 24 hours.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "body": {
                    "type": "string"
                  },
                  "internal_only": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated message",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message"
                  ],
                  "properties": {
                    "message": {
                      "$ref": "#/components/schemas/WorkspaceMessage"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The app or the message board is disabled, the caller is not a member, the caller is a read-only viewer, or the caller is neither the message's author nor a workspace admin (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such workspace or message (error code `not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The idempotency key was reused for a different request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Nothing was written (error code `validation_failed`) — the body carried none of the editable keys, or the values are invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{workspace_id}/messages/{message_id}/attachments": {
      "parameters": [
        {
          "name": "workspace_id",
          "in": "path",
          "required": true,
          "description": "The workspace's numeric id OR its slug.",
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "message_id",
          "in": "path",
          "required": true,
          "description": "The message id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Workspace"
        ],
        "summary": "Attach a file to a message",
        "description": "Adds one file to a message authored by the caller. Workspace app admins\nmay manage any message. The upload follows the same Drive attachment\npipeline as the web composer.\n\nSend the body as `multipart/form-data`. Ordinary files may be up to\n10 MB; files whose media type is video may be up to 100 MB. Upload each\nfile in a separate request so retries and failures are isolated. If the\nsame file bytes are already attached, the API returns that attachment\nwith `200` and `replayed: true` instead of storing a duplicate.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Identical file bytes were already attached; the existing attachment was replayed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "attachment",
                    "message",
                    "replayed"
                  ],
                  "properties": {
                    "attachment": {
                      "$ref": "#/components/schemas/WorkspaceMessageAttachment"
                    },
                    "message": {
                      "$ref": "#/components/schemas/WorkspaceMessage"
                    },
                    "replayed": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "File attached",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "attachment",
                    "message",
                    "replayed"
                  ],
                  "properties": {
                    "attachment": {
                      "$ref": "#/components/schemas/WorkspaceMessageAttachment"
                    },
                    "message": {
                      "$ref": "#/components/schemas/WorkspaceMessage"
                    },
                    "replayed": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No multipart file was supplied (error code `invalid_file`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller cannot contribute to or manage this message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such workspace or message (error code `not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The file failed validation or storage (error code `attachment_failed`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{workspace_id}/messages/{message_id}/attachments/{id}": {
      "parameters": [
        {
          "name": "workspace_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "message_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The Drive attachment id returned by the message API.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "delete": {
        "tags": [
          "Workspace"
        ],
        "summary": "Remove a message attachment",
        "description": "Soft-deletes one attachment. Author-only, or a Workspace app admin.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Retry key retained by the platform for 24 hours.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Attachment removed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "removed_attachment_id",
                    "message"
                  ],
                  "properties": {
                    "removed_attachment_id": {
                      "type": "integer"
                    },
                    "message": {
                      "$ref": "#/components/schemas/WorkspaceMessage"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller cannot contribute to or manage this message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such workspace, message, or active attachment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The idempotency key was reused for a different request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The attachment could not be removed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{workspace_id}/tasks": {
      "parameters": [
        {
          "name": "workspace_id",
          "in": "path",
          "required": true,
          "description": "The workspace's numeric id OR its slug.",
          "schema": {
            "type": "string"
          },
          "example": "42"
        }
      ],
      "get": {
        "tags": [
          "Workspace"
        ],
        "summary": "List workspace tasks",
        "description": "The workspace's canvas tasks. Distinct from the org-wide Tasks API at\n`/api/v1/tasks`: both read the same underlying records, and these are\nthe ones attached to a workspace.\n\nOrdered by due date, undated last, with a stable tiebreaker — so\npaging never shows the same task twice or skips one.\n\nRequires the business-level `enable_tasks` toggle.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "`active` (the default — not completed), `completed`, `overdue` (past due and not completed), or `all`. An unrecognised value behaves as `active`.",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "completed",
                "overdue",
                "all"
              ],
              "default": "active"
            }
          },
          {
            "name": "assignee_id",
            "in": "query",
            "required": false,
            "description": "Only tasks assigned to this user.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Rows per page. Maximum 200.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Alias for `per_page`, kept because it is what these endpoints shipped with. `per_page` wins when both are sent. A blank or non-numeric value falls back to the endpoint's default.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Tasks on this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "tasks",
                    "total_count",
                    "meta"
                  ],
                  "properties": {
                    "tasks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WorkspaceTask"
                      }
                    },
                    "total_count": {
                      "type": "integer",
                      "description": "Tasks matching the filters, across all pages."
                    },
                    "meta": {
                      "$ref": "#/components/schemas/WorkspacePaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Workspace app is not enabled for the business or not accessible to the caller, the caller is not a member of this workspace, or the canvas section this endpoint serves is switched off for the business (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No workspace with that id or slug is reachable by the caller (error code `not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Workspace"
        ],
        "summary": "Create a task",
        "description": "Adds a task to the workspace. CONTRIBUTORS ONLY.\n\n`task_list_id` (alias `list_id`) is OPTIONAL. A workspace created\nthrough this API has no task list, and this namespace exposes no\ntask-list resource — so when none is given the first active list is\nreused, or a list named \"Default\" is created. Supplying a list id that\nis not in this workspace is `404`, which is distinct from supplying\nnone.\n\nAn `assignee_id` that resolves to nobody in the business is `404`, not\na silent unassigned create; an unparseable `due_at` is `422`, not a\nsilent \"no due date\". Assigning a non-member is refused by the service\nwith `422` naming the reason.\n\nRequires the business-level `enable_tasks` toggle.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "title"
                ],
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "notes": {
                    "type": "string",
                    "nullable": true
                  },
                  "assignee_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "A user in this business who is a member of the workspace."
                  },
                  "due_at": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true,
                    "description": "ISO 8601 datetime."
                  },
                  "task_list_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "A task list in THIS workspace. Omit to use (or create) the workspace's default list. `list_id` is accepted as an alias."
                  },
                  "list_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Alias for `task_list_id`."
                  },
                  "internal_only": {
                    "type": "boolean",
                    "default": false,
                    "description": "Excludes the task from the client digest and the client portal."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created task",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "task"
                  ],
                  "properties": {
                    "task": {
                      "$ref": "#/components/schemas/WorkspaceTask"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The app or Tasks is disabled, the caller is not a member, or the caller is a read-only viewer (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such workspace, the given `task_list_id` is not in this workspace, or the given `assignee_id` resolves to nobody in the business (error code `not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Nothing was created (error code `validation_failed`) — an unparseable `due_at`, an assignee who is not a workspace member, an invalid title, or no default task list could be created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{workspace_id}/tasks/{id}": {
      "parameters": [
        {
          "name": "workspace_id",
          "in": "path",
          "required": true,
          "description": "The workspace's numeric id OR its slug.",
          "schema": {
            "type": "string"
          },
          "example": "42"
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The task id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "patch": {
        "tags": [
          "Workspace"
        ],
        "summary": "Edit a task",
        "description": "Retitle, re-note, reassign, reschedule, change the recurrence, or move\nthe task to another list in the SAME workspace. CONTRIBUTORS ONLY —\nany contributor may edit any task on the canvas (unlike messages,\nwhich are author-only).\n\n**Only the keys you send are written**, so a request carrying just\n`title` will not clear the due date or unassign the task. Send at least\none editable key, or the request is refused with `422`. Send\n`assignee_id: null` or `due_at: null` to clear those explicitly.\n\nAn `assignee_id` that resolves to nobody is `404`; an unparseable\n`due_at` is `422` rather than a silent clear. `PUT` behaves\nidentically.\n\nRequires the business-level `enable_tasks` toggle.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "notes": {
                    "type": "string",
                    "nullable": true
                  },
                  "assignee_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Null unassigns. An id that resolves to nobody is `404`."
                  },
                  "due_at": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true,
                    "description": "ISO 8601 datetime. Null clears the due date."
                  },
                  "recurring_cadence": {
                    "type": "string",
                    "nullable": true
                  },
                  "internal_only": {
                    "type": "boolean"
                  },
                  "task_list_id": {
                    "type": "integer",
                    "description": "A task list in this workspace. `list_id` is accepted as an alias."
                  },
                  "list_id": {
                    "type": "integer",
                    "description": "Alias for `task_list_id`."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated task",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "task"
                  ],
                  "properties": {
                    "task": {
                      "$ref": "#/components/schemas/WorkspaceTask"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The app or Tasks is disabled, the caller is not a member, or the caller is a read-only viewer (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such workspace or task, or the given `assignee_id` resolves to nobody in the business (error code `not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Nothing was written (error code `validation_failed`) — the body carried none of the editable keys, `due_at` is unparseable, or the values are invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{workspace_id}/tasks/{id}/complete": {
      "parameters": [
        {
          "name": "workspace_id",
          "in": "path",
          "required": true,
          "description": "The workspace's numeric id OR its slug.",
          "schema": {
            "type": "string"
          },
          "example": "42"
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The task id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Workspace"
        ],
        "summary": "Complete a task",
        "description": "Marks the task complete. CONTRIBUTORS ONLY. Idempotent enough to retry:\nthe response always carries the task's current state.\n\nRequires the business-level `enable_tasks` toggle.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The completed task",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "task"
                  ],
                  "properties": {
                    "task": {
                      "$ref": "#/components/schemas/WorkspaceTask"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The app or Tasks is disabled, the caller is not a member, or the caller is a read-only viewer (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such workspace or task (error code `not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The task was not completed (error code `validation_failed`). `error.details` carries both the `errors` list and the task's current `task` state, so a client can re-render without a second call.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{workspace_id}/tasks/{id}/reopen": {
      "parameters": [
        {
          "name": "workspace_id",
          "in": "path",
          "required": true,
          "description": "The workspace's numeric id OR its slug.",
          "schema": {
            "type": "string"
          },
          "example": "42"
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The task id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Workspace"
        ],
        "summary": "Reopen a task",
        "description": "The undo for complete — clears the completion. CONTRIBUTORS ONLY.\n\nRequires the business-level `enable_tasks` toggle.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The reopened task",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "task"
                  ],
                  "properties": {
                    "task": {
                      "$ref": "#/components/schemas/WorkspaceTask"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The app or Tasks is disabled, the caller is not a member, or the caller is a read-only viewer (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such workspace or task (error code `not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The task was not reopened (error code `validation_failed`). `error.details` carries both the `errors` list and the task's current `task` state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{workspace_id}/check-ins": {
      "parameters": [
        {
          "name": "workspace_id",
          "in": "path",
          "required": true,
          "description": "The workspace's numeric id OR its slug.",
          "schema": {
            "type": "string"
          },
          "example": "42"
        }
      ],
      "get": {
        "tags": [
          "Workspace"
        ],
        "summary": "List recurring check-ins",
        "description": "The workspace's ACTIVE recurring check-ins — the questions the app asks\nmembers on a cadence — with the next scheduled run for each.\n\nRequires the business-level `enable_check_ins` toggle.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Rows per page. Maximum 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Alias for `per_page`, kept because it is what these endpoints shipped with. `per_page` wins when both are sent. A blank or non-numeric value falls back to the endpoint's default.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Active check-ins",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "check_ins",
                    "total_count",
                    "meta"
                  ],
                  "properties": {
                    "check_ins": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WorkspaceCheckIn"
                      }
                    },
                    "total_count": {
                      "type": "integer",
                      "description": "Active check-ins on this workspace, across all pages."
                    },
                    "meta": {
                      "$ref": "#/components/schemas/WorkspacePaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Workspace app is not enabled for the business or not accessible to the caller, the caller is not a member of this workspace, or the canvas section this endpoint serves is switched off for the business (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No workspace with that id or slug is reachable by the caller (error code `not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{workspace_id}/check-ins/my-drafts": {
      "parameters": [
        {
          "name": "workspace_id",
          "in": "path",
          "required": true,
          "description": "The workspace's numeric id OR its slug.",
          "schema": {
            "type": "string"
          },
          "example": "42"
        }
      ],
      "get": {
        "tags": [
          "Workspace"
        ],
        "summary": "My AI-drafted check-in responses awaiting approval",
        "description": "The CALLER's own AI-drafted check-in responses for this workspace that\nhave not been committed yet — the queue behind the \"review your draft\"\nprompt on the web and mobile surfaces. Never another user's drafts.\n\nApprove one with the `.../responses/{response_id}/approve` endpoint;\nit then disappears from this list.\n\nRequires the business-level `enable_check_ins` toggle.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Rows per page. Maximum 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Alias for `per_page`, kept because it is what these endpoints shipped with. `per_page` wins when both are sent. A blank or non-numeric value falls back to the endpoint's default.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's pending drafts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "drafts",
                    "total_count",
                    "meta"
                  ],
                  "properties": {
                    "drafts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WorkspaceCheckInDraft"
                      }
                    },
                    "total_count": {
                      "type": "integer",
                      "description": "The caller's pending drafts here, across all pages."
                    },
                    "meta": {
                      "$ref": "#/components/schemas/WorkspacePaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Workspace app is not enabled for the business or not accessible to the caller, the caller is not a member of this workspace, or the canvas section this endpoint serves is switched off for the business (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No workspace with that id or slug is reachable by the caller (error code `not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{workspace_id}/check-ins/responses/{response_id}/approve": {
      "parameters": [
        {
          "name": "workspace_id",
          "in": "path",
          "required": true,
          "description": "The workspace's numeric id OR its slug.",
          "schema": {
            "type": "string"
          },
          "example": "42"
        },
        {
          "name": "response_id",
          "in": "path",
          "required": true,
          "description": "The draft check-in response id, from `my-drafts`.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Workspace"
        ],
        "summary": "Approve an AI-drafted check-in response",
        "description": "Commits one of the caller's AI drafts as their real check-in response,\noptionally replacing the text first. CONTRIBUTORS ONLY.\n\n**Only your own draft.** Someone else's is `403`, and a draft that\nlives in a different workspace is `404` even if you are a member of\nboth.\n\nRequires the business-level `enable_check_ins` toggle. Check-ins can\nadditionally be turned off for a SINGLE workspace by its owner; that\nrefusal comes back as `422` with the reason, distinct from the\nbusiness-level `403`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "response_text": {
                    "type": "string",
                    "description": "Replaces the drafted text before committing. Omit to commit the draft as written."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The committed response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "response"
                  ],
                  "properties": {
                    "response": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "responded_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "Set once the draft is committed."
                        },
                        "response_text": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The app or check-ins are disabled, the caller is not a member, the caller is a read-only viewer, or the draft belongs to another user (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such workspace, or no such draft in THIS workspace (error code `not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The draft was not committed. Either check-ins are turned off for this specific workspace (error code `validation_failed`, with the reason), or approval failed internally (error code `approval_failed`, with a fixed client-safe message — the detail is logged server-side).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{workspace_id}/events": {
      "parameters": [
        {
          "name": "workspace_id",
          "in": "path",
          "required": true,
          "description": "The workspace's numeric id OR its slug.",
          "schema": {
            "type": "string"
          },
          "example": "42"
        }
      ],
      "get": {
        "tags": [
          "Workspace"
        ],
        "summary": "List schedule events",
        "description": "The workspace's schedule. `upcoming` (the default) returns events that\nhave not started yet, soonest first; `upcoming=false` returns PAST\nevents, most recent first — it is a genuine \"past events\" tab, not the\nsame list reversed, and `total_count` counts only the half you asked\nfor.\n\n`internal_only` is carried on every row: `location` is where meeting\nlinks live, so an integration building a client-facing calendar needs\nto know which entries must not leave the company.\n\nRequires the business-level `enable_schedule` toggle.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "upcoming",
            "in": "query",
            "required": false,
            "description": "`true` (the default) for events yet to start; `false` for past events. Accepts JSON booleans and the usual string spellings.",
            "schema": {
              "type": "boolean",
              "default": true
            }
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Rows per page. Maximum 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Alias for `per_page`, kept because it is what these endpoints shipped with. `per_page` wins when both are sent. A blank or non-numeric value falls back to the endpoint's default.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Schedule events",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "events",
                    "total_count",
                    "meta"
                  ],
                  "properties": {
                    "events": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WorkspaceEvent"
                      }
                    },
                    "total_count": {
                      "type": "integer",
                      "description": "Events in the requested half (upcoming OR past), across all pages."
                    },
                    "meta": {
                      "$ref": "#/components/schemas/WorkspacePaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Workspace app is not enabled for the business or not accessible to the caller, the caller is not a member of this workspace, or the canvas section this endpoint serves is switched off for the business (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No workspace with that id or slug is reachable by the caller (error code `not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{workspace_id}/events/{id}": {
      "parameters": [
        {
          "name": "workspace_id",
          "in": "path",
          "required": true,
          "description": "The workspace's numeric id OR its slug.",
          "schema": {
            "type": "string"
          },
          "example": "42"
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The event id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Workspace"
        ],
        "summary": "One schedule event",
        "description": "A single event on this workspace's schedule.\n\nRequires the business-level `enable_schedule` toggle.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The event",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "event"
                  ],
                  "properties": {
                    "event": {
                      "$ref": "#/components/schemas/WorkspaceEvent"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Workspace app is not enabled for the business or not accessible to the caller, the caller is not a member of this workspace, or the canvas section this endpoint serves is switched off for the business (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such workspace or event (error code `not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{workspace_id}/hill-charts": {
      "parameters": [
        {
          "name": "workspace_id",
          "in": "path",
          "required": true,
          "description": "The workspace's numeric id OR its slug.",
          "schema": {
            "type": "string"
          },
          "example": "42"
        }
      ],
      "get": {
        "tags": [
          "Workspace"
        ],
        "summary": "List hill charts",
        "description": "The workspace's ACTIVE hill charts — each a named scope and its\nposition on the \"figuring it out / making it happen\" curve.\n\nWhen the Workspace agent has proposed a new position but nobody has\naccepted it yet, `ai_proposal_pending` is true and\n`ai_proposed_position` carries the suggestion; `position` still holds\nthe human-set value.\n\nRequires the business-level `enable_hill_charts` toggle.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Rows per page. Maximum 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Alias for `per_page`, kept because it is what these endpoints shipped with. `per_page` wins when both are sent. A blank or non-numeric value falls back to the endpoint's default.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Active hill charts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "hill_charts",
                    "total_count",
                    "meta"
                  ],
                  "properties": {
                    "hill_charts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WorkspaceHillChart"
                      }
                    },
                    "total_count": {
                      "type": "integer",
                      "description": "Active hill charts on this workspace, across all pages."
                    },
                    "meta": {
                      "$ref": "#/components/schemas/WorkspacePaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Workspace app is not enabled for the business or not accessible to the caller, the caller is not a member of this workspace, or the canvas section this endpoint serves is switched off for the business (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No workspace with that id or slug is reachable by the caller (error code `not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{workspace_id}/hill-charts/{id}": {
      "parameters": [
        {
          "name": "workspace_id",
          "in": "path",
          "required": true,
          "description": "The workspace's numeric id OR its slug.",
          "schema": {
            "type": "string"
          },
          "example": "42"
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The hill chart id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Workspace"
        ],
        "summary": "One hill chart",
        "description": "A single hill chart. Unlike the list, this reaches ARCHIVED charts too\n— `status` says which.\n\nRequires the business-level `enable_hill_charts` toggle.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The hill chart",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "hill_chart"
                  ],
                  "properties": {
                    "hill_chart": {
                      "$ref": "#/components/schemas/WorkspaceHillChart"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Workspace app is not enabled for the business or not accessible to the caller, the caller is not a member of this workspace, or the canvas section this endpoint serves is switched off for the business (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such workspace or hill chart (error code `not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{workspace_id}/reactions": {
      "parameters": [
        {
          "name": "workspace_id",
          "in": "path",
          "required": true,
          "description": "The workspace's numeric id OR its slug.",
          "schema": {
            "type": "string"
          },
          "example": "42"
        }
      ],
      "get": {
        "tags": [
          "Workspace"
        ],
        "summary": "List reactions on one item",
        "description": "Every reaction on ONE reactable item, oldest first, with the reacting\nuser on each row. Both `reactable_type` and `reactable_id` are\nrequired — this lists the reactions on a specific message, comment or\ntask, not the workspace's reactions in bulk.\n\nThe item must live in THIS workspace; one from another workspace is\n`404` even if the caller can see it elsewhere.\n\nRequires the business-level `enable_reactions` toggle.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "reactable_type",
            "in": "query",
            "required": true,
            "description": "The kind of item to read reactions for.",
            "schema": {
              "type": "string",
              "enum": [
                "WorkspaceMessage",
                "WorkspaceMessageComment",
                "Task"
              ]
            }
          },
          {
            "name": "reactable_id",
            "in": "query",
            "required": true,
            "description": "The id of the item, which must belong to this workspace.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Rows per page. Maximum 200.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 100
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Alias for `per_page`, kept because it is what these endpoints shipped with. `per_page` wins when both are sent. A blank or non-numeric value falls back to the endpoint's default.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Reactions on the item",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "reactions",
                    "total_count",
                    "meta"
                  ],
                  "properties": {
                    "reactions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WorkspaceReaction"
                      }
                    },
                    "total_count": {
                      "type": "integer",
                      "description": "Reactions on this item, across all pages."
                    },
                    "meta": {
                      "$ref": "#/components/schemas/WorkspacePaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Workspace app is not enabled for the business or not accessible to the caller, the caller is not a member of this workspace, or the canvas section this endpoint serves is switched off for the business (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such workspace, or `reactable_type` is not one of the allowed types, or no item with that id in this workspace (error code `not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{workspace_id}/reactions/toggle": {
      "parameters": [
        {
          "name": "workspace_id",
          "in": "path",
          "required": true,
          "description": "The workspace's numeric id OR its slug.",
          "schema": {
            "type": "string"
          },
          "example": "42"
        }
      ],
      "post": {
        "tags": [
          "Workspace"
        ],
        "summary": "Add or remove a reaction",
        "description": "Toggles ONE emoji by the calling user on one item: present becomes\nabsent, absent becomes present. `action` says which happened, and\n`counts` returns the item's full per-emoji tally so a client can\nre-render without a second call. CONTRIBUTORS ONLY.\n\nThe emoji must be in the item's allowed set; anything else is `422`.\nConcurrent toggles are safe — a duplicate insert is treated as an add.\n\nRequires the business-level `enable_reactions` toggle.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "reactable_type",
                  "reactable_id",
                  "emoji"
                ],
                "properties": {
                  "reactable_type": {
                    "type": "string",
                    "enum": [
                      "WorkspaceMessage",
                      "WorkspaceMessageComment",
                      "Task"
                    ]
                  },
                  "reactable_id": {
                    "type": "integer",
                    "description": "The id of the item, which must belong to this workspace."
                  },
                  "emoji": {
                    "type": "string",
                    "description": "One emoji from the item's allowed set.",
                    "example": "👍"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The toggle result and the item's new tally",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "action",
                    "counts"
                  ],
                  "properties": {
                    "action": {
                      "type": "string",
                      "enum": [
                        "added",
                        "removed"
                      ]
                    },
                    "counts": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "integer"
                      },
                      "description": "Reaction count per emoji on this item, after the toggle.",
                      "example": {
                        "👍": 3,
                        "🎉": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The app or reactions are disabled, the caller is not a member, or the caller is a read-only viewer (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such workspace, or `reactable_type` is not one of the allowed types, or no item with that id in this workspace (error code `not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The emoji is not in the item's allowed set (error code `validation_failed`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/ideas": {
      "post": {
        "tags": [
          "Ideas"
        ],
        "summary": "Post a new idea",
        "description": "Creates an idea — the native mirror of the web composer\n(`Apps::Ideas::IdeasController#create`), sharing\n`Ideas::IdeaCreationService` and `Ideas::AttachmentScreener` so the two\nsurfaces produce identical rows.\n\n**Side effects, all matching the web:**\n* the idea lands in the workspace's **entry** lifecycle stage (a new idea is\n  never dropped straight into Reviewing/Planned);\n* the **author auto-votes**, so a brand-new idea comes back with\n  `vote_count: 1` and `has_voted: true` (the web's *\"Idea posted — you're the\n  first vote!\"*);\n* `description_html` is derived from the plain text (escaped first, then\n  formatted, so line breaks survive and any markup the author typed stays\n  inert text);\n* an audit entry is written and the review panel is notified in the\n  background.\n\n**Attachments are gated.** `files[]` / `file_signed_ids[]` are accepted only\nwhile **\"Allow file attachments\"** is ON for the workspace. When it is OFF the\nfiles are **dropped and the idea is still created** — the web behaves the same\nway (its composer simply renders no file field) — and the drop is **named\nper file in `attachment_errors` and repeated in `warnings`**. Accepted files\nare screened against a size cap and a sniffed-content-type allowlist; anything\ndropped is reported the same way. `attachment_errors` is always present (empty\nwhen everything attached) because a silently missing attachment is the worst\noutcome.\n\n**`warnings` names every field this workspace's settings discarded.** A 2xx\nthat dropped part of the write says so: a file the attachments toggle refused,\na `voting_closes_on` the close-date toggle refused, a `campaign_id` the\ncampaigns toggle refused. The key is omitted entirely when nothing was\ndropped, so its presence is the signal. `GET /api/v1/ideas/config` advertises\nthe same toggles if a client would rather not send the field at all.\n\n**Authorization** is the workspace's *\"Who can submit ideas\"* audience\n(`submit_audience`) — everyone, or only one configured group. Reading the\nIdeas app is not enough: a user can browse ideas and still be outside the\nposting audience (`403 forbidden`).\n\n**Duplicate detection** mirrors the web composer. Before creating, the title\nis matched against existing ideas (the SAME matcher — `Ideas::DuplicateFinder`\n— behind the web's nudge and its typeahead). If it looks like one or more\nexisting ideas and the caller hasn't confirmed, the endpoint answers\n**`409`** with the matches — `{ duplicates_found: true, message,\nduplicates: [{ id, title, vote_count, has_voted }] }` — and **creates\nnothing**. Show them (\"add your vote, or post anyway\"), then re-POST with\n**`confirm_duplicates: true`** to post anyway. Detection runs only for an\notherwise-valid idea, so a missing field still answers `422`, not `409`.\n\nResponds **201** with the SAME canonical idea object\n`GET /api/v1/ideas/{id}` returns.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "description": "`application/json` for a text-only idea; `multipart/form-data` when posting `files[]`.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "title",
                  "description"
                ],
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Required. Surrounding whitespace is stripped.",
                    "example": "Dark mode for the mobile app"
                  },
                  "description": {
                    "type": "string",
                    "description": "Required — the model validates its presence on create.",
                    "example": "Please add a dark theme; the white background is rough on night shift."
                  },
                  "campaign_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Optional. Honoured only while the Campaigns feature is on — silently ignored when it is off, so a client keeps working if the workspace later disables campaigns. Must belong to this business and still be open, else `422`.",
                    "example": 12
                  },
                  "voting_closes_on": {
                    "type": "string",
                    "format": "date",
                    "nullable": true,
                    "description": "Optional. Honoured only while the voting-close-date feature is on; ignored otherwise. Must be on or before the campaign's close date.",
                    "example": "2026-09-30"
                  },
                  "confirm_duplicates": {
                    "type": "boolean",
                    "default": false,
                    "description": "Set `true` to skip duplicate detection and post even when a similar idea already exists (the web's \"post anyway\"). When omitted/false, a title that matches existing ideas answers `409` with the matches instead of creating.",
                    "example": false
                  }
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "title",
                  "description"
                ],
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 200
                  },
                  "description": {
                    "type": "string"
                  },
                  "campaign_id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "voting_closes_on": {
                    "type": "string",
                    "format": "date",
                    "nullable": true
                  },
                  "confirm_duplicates": {
                    "type": "boolean",
                    "default": false,
                    "description": "Set `true` to skip duplicate detection and post anyway (the web's \"post anyway\"). Otherwise a matching title answers `409`."
                  },
                  "files[]": {
                    "type": "array",
                    "description": "Files to attach. Accepted ONLY while \"Allow file attachments\" is ON; otherwise dropped (the idea is still created). Screened against a size cap and a sniffed content-type allowlist (images, PDF, Word/Excel/PowerPoint, plain text).",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    }
                  },
                  "file_signed_ids[]": {
                    "type": "array",
                    "description": "Alternative to `files[]` for clients that direct-upload first: ActiveStorage signed ids. Re-screened server-side, and an already-attached blob is rejected, so a signed id cannot be replayed to steal another record's file.",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Idea created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "idea",
                    "attachment_errors"
                  ],
                  "properties": {
                    "idea": {
                      "type": "object",
                      "description": "The created idea, in the SAME shape `GET /api/v1/ideas/{id}` returns — one query object and one serializer back both, so the two cannot drift. See that endpoint for the full field list. Note `vote_count` is 1 and `has_voted` is true on a fresh create, because the author auto-votes.",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 91
                        },
                        "title": {
                          "type": "string",
                          "example": "Dark mode for the mobile app"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true
                        },
                        "description_html": {
                          "type": "string",
                          "nullable": true
                        },
                        "vote_count": {
                          "type": "integer",
                          "example": 1
                        },
                        "has_voted": {
                          "type": "boolean",
                          "example": true
                        },
                        "stage": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 1
                            },
                            "label": {
                              "type": "string",
                              "example": "New"
                            }
                          }
                        }
                      }
                    },
                    "attachment_errors": {
                      "type": "array",
                      "description": "One human-readable reason per file that was NOT attached (wrong type, over the size cap, or the workspace's \"Allow file attachments\" setting being off). Always present; empty when everything attached. Never fatal — the idea is created either way.",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "payload.zip is not a supported file type and wasn't attached."
                      ]
                    },
                    "warnings": {
                      "type": "array",
                      "description": "One sentence per field this workspace's settings discarded — dropped files, a refused `voting_closes_on`, a refused `campaign_id`. OMITTED when nothing was dropped, so the key's presence is the signal that the 2xx is a partial write.",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "\"Allow ideas to have a voting close date\" is off for this workspace — voting_closes_on was ignored and no close date was set."
                      ]
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller is outside the workspace's submit audience (error code `forbidden`), or the Ideas app is not accessible to them (`access_denied`)."
          },
          "409": {
            "description": "The title matches one or more existing ideas and `confirm_duplicates` was not set — the matches are returned and **nothing was created**. Re-POST with `confirm_duplicates: true` to post anyway.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "duplicates_found",
                    "message",
                    "duplicates"
                  ],
                  "properties": {
                    "duplicates_found": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "description": "Human-readable summary, e.g. the web's nudge headline.",
                      "example": "2 similar ideas already exist"
                    },
                    "duplicates": {
                      "type": "array",
                      "description": "The matching ideas, most-likely first (max 3). Just enough to render the \"add your vote, or post anyway\" panel.",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "title",
                          "vote_count",
                          "has_voted"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 74
                          },
                          "title": {
                            "type": "string",
                            "example": "Dark mode for the mobile app"
                          },
                          "vote_count": {
                            "type": "integer",
                            "description": "The idea's current upvote count.",
                            "example": 12
                          },
                          "has_voted": {
                            "type": "boolean",
                            "description": "Whether the CALLING user has already upvoted this idea.",
                            "example": false
                          }
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation failed (error code `invalid`) — a missing/blank title, a title over 200 characters, a missing description, or a campaign that is closed or belongs to another business. Also `no_entry_stage` when the workspace has no entry lifecycle stage configured. Also `content_blocked` when the workspace's content-moderation policy refuses the submitted text; the message is the policy's own and is safe to show the author verbatim."
          }
        }
      }
    },
    "/ideas/dashboard": {
      "get": {
        "tags": [
          "Ideas"
        ],
        "summary": "Ideas dashboard",
        "description": "The native-client mirror of the web Ideas dashboard\n(`Apps::IdeasController#show`). Every number and list is produced by the\nSAME query object the web view uses (`Ideas::DashboardStats`), so the two\nsurfaces cannot drift.\n\n**Persona-independent.** Unlike some dashboards, the web Ideas dashboard\napplies NO persona / visibility / status filter — an idea has no\ndraft/published state and is live the moment it is created. Admins,\nreviewers and regular members therefore receive the **identical**\npayload. There is no `is_admin` branch.\n\n**Sections** (each list is capped at 5):\n* `total_ideas` — business-scoped count of all ideas.\n* `count_by_stage` — one row per lifecycle stage in pipeline order\n  (`position` asc), with `count` defaulting to 0 — exactly the per-stage\n  tiles the web renders. The counts sum to `total_ideas` (every idea is\n  in exactly one stage).\n* `top_voted` — highest-voted first (the `up_votes_count` counter cache;\n  positive votes only). Fields: id, title, votes, stage.\n* `recently_added` — newest first by **`created_at`** (the date the web\n  renders as \"N ago\"; there is no separate published/submitted date).\n  Fields: id, title, creator_name (the author's full name), created_at.\n* `most_discussed` — most-commented first (all non-deleted\n  `Platform::Comment` on the idea, threaded replies included).\n  Zero-comment ideas are **dropped** from this section, matching the web,\n  so it can contain fewer than 5 (or be empty) in a quiet tenant.\n  Fields: id, title, comments_count.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Dashboard retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "dashboard"
                  ],
                  "properties": {
                    "dashboard": {
                      "type": "object",
                      "required": [
                        "total_ideas",
                        "count_by_stage",
                        "top_voted",
                        "recently_added",
                        "most_discussed"
                      ],
                      "properties": {
                        "total_ideas": {
                          "type": "integer",
                          "description": "Count of all ideas in the business.",
                          "example": 42
                        },
                        "count_by_stage": {
                          "type": "array",
                          "description": "One row per lifecycle stage, in pipeline (position) order. Counts sum to total_ideas.",
                          "items": {
                            "type": "object",
                            "required": [
                              "stage_id",
                              "label",
                              "count"
                            ],
                            "properties": {
                              "stage_id": {
                                "type": "integer",
                                "example": 1
                              },
                              "label": {
                                "type": "string",
                                "example": "New"
                              },
                              "color": {
                                "type": "string",
                                "description": "Stage colour (hex).",
                                "example": "#6c757d"
                              },
                              "count": {
                                "type": "integer",
                                "description": "Ideas currently in this stage (0 when empty).",
                                "example": 12
                              }
                            }
                          }
                        },
                        "top_voted": {
                          "type": "array",
                          "description": "Up to 5 ideas, highest up-votes first.",
                          "items": {
                            "type": "object",
                            "required": [
                              "id",
                              "title",
                              "votes",
                              "stage"
                            ],
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 87
                              },
                              "title": {
                                "type": "string",
                                "example": "Dark mode for the mobile app"
                              },
                              "votes": {
                                "type": "integer",
                                "description": "Positive-vote total (up_votes_count).",
                                "example": 34
                              },
                              "stage": {
                                "type": "object",
                                "required": [
                                  "id",
                                  "label"
                                ],
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 2
                                  },
                                  "label": {
                                    "type": "string",
                                    "example": "Reviewing"
                                  }
                                }
                              }
                            }
                          }
                        },
                        "recently_added": {
                          "type": "array",
                          "description": "Up to 5 ideas, newest created_at first.",
                          "items": {
                            "type": "object",
                            "required": [
                              "id",
                              "title",
                              "creator_name",
                              "created_at"
                            ],
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 91
                              },
                              "title": {
                                "type": "string",
                                "example": "Add SSO for contractors"
                              },
                              "creator_name": {
                                "type": "string",
                                "description": "The author's full name.",
                                "example": "Dana Lee"
                              },
                              "created_at": {
                                "type": "string",
                                "format": "date-time",
                                "description": "When the idea was created (rendered as \"N ago\" on web).",
                                "example": "2026-07-31T06:01:58Z"
                              }
                            }
                          }
                        },
                        "most_discussed": {
                          "type": "array",
                          "description": "Up to 5 ideas with the most comments (replies included), highest first. Zero-comment ideas are excluded, so this may be shorter than 5 or empty.",
                          "items": {
                            "type": "object",
                            "required": [
                              "id",
                              "title",
                              "comments_count"
                            ],
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 87
                              },
                              "title": {
                                "type": "string",
                                "example": "Dark mode for the mobile app"
                              },
                              "comments_count": {
                                "type": "integer",
                                "description": "Non-deleted comments + replies.",
                                "example": 9
                              }
                            }
                          }
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Ideas app is not accessible to the caller (error code `access_denied`)."
          }
        }
      }
    },
    "/ideas/config": {
      "get": {
        "tags": [
          "Ideas"
        ],
        "summary": "Ideas workspace configuration",
        "description": "The tenant's Ideas configuration **as this caller experiences it** — the\nnative mirror of what every web Ideas screen reads from\n`IdeasAppConfigurable`, and of what an admin edits at **Apps ▸ Ideas ▸\nSettings**. One call, so a client can render the app the way this workspace\nis configured without probing endpoint by endpoint.\n\n**Persona-aware, not persona-branched.** Every caller receives the same\nkeys. The two `can_*` booleans are resolved **for the caller** under the\nexact rule the write sites enforce, so a client can show the *Submit Idea*\nand *New Campaign* affordances precisely when the server would accept\nthem. There is **no admin bypass** on either: an admin configures the\naudience, they don't override it.\n\n**Audiences** (`submit_audience`, `campaign_creators`) are each reported as\n`{ value, type, group }`:\n* `type: \"all\"` — everyone in the business may do it (the default, and what\n  a blank setting means).\n* `type: \"group\"` — only members of `group` may. `group` is `null` when the\n  saved group has since been deleted or belongs to another business; the\n  runtime gate denies in that case, so `can_submit_idea` /\n  `can_create_campaign` is `false` — **trust the `can_*` boolean**, never\n  infer permission from `type`.\n\n**Review panels** (`idea_reviewers`, `campaign_reviewers`) are each\n`{ group, count, source }`:\n* `count` is the panel's FULL membership size. Neither panel **names anyone**\n  — there is no `members` key and the shape does not change with\n  `reviewer_names_visible`. Render \"Reviewed by <group.name> (<count>)\" from\n  this, and call a roster endpoint for the actual people — both paginated,\n  name-searchable, and gated on `reviewer_names_visible`:\n  `GET /ideas/{idea_id}/reviewers` for a given idea's panel, and\n  `GET /ideas/campaigns/{id}/reviewers` for a given campaign's.\n* `idea_reviewers.source` is `configured` when Settings picked the group, or\n  `fallback` when no group is saved and ideas therefore route to the\n  built-in **All Admins** group.\n* `campaign_reviewers.source` is `configured` when Settings picked a\n  campaign panel, or `inherited` when it is blank — the Settings option\n  \"— Same as the Idea Reviewers group —\", so the group echoes\n  `idea_reviewers.group`. This is the panel a NEW campaign pre-fills with;\n  a creator may override it per campaign.\n* `group` is `null` (with `count: 0`) only when nothing resolves at all —\n  no saved group and no All Admins group to fall back to.\n\n**`campaign_options`** is the option data a \"New Campaign\" composer needs so\nit can only offer values `POST /ideas/campaigns` accepts: the authored icon\nset, the accent palette with its hex values, this workspace's selectable\nreviewer groups, and the defaults each field falls back to when omitted. The\nreviewer-group list is empty while `campaigns_enabled` is `false`.\n\n**Stages** is the complete lifecycle pipeline in order (`position`\nascending). `id` is stable across a rename, so a client may cache stage ids;\n`name` is the admin-editable label; `category`\n(`entry`/`active`/`implemented`/`declined`) is what outcome metrics key off\n— never the id. `icon` is the stage's Font Awesome glyph name with **no `fa-`\nprefix**, DERIVED from the label and category (there is no icon column and\nadmins never pick one) — the same glyph the web renders for that stage, so a\nnative pipeline looks like the web one. See `IdeaLifecycleStage` for the exact\nderivation. A workspace that has never opened Ideas gets the shipped default\npipeline seeded on first read, so this array is never empty.\n\n**`accent`** is the workspace's accent colour, shipped as its key **and** the\nfive colour tokens every web Ideas screen renders from — tint your own chrome\nfrom those tokens rather than a client-side palette, which would drift from the\nweb the moment a token is re-tuned. See the field below for the coercion rule.\n\n**Deliberately not included:** the web-chrome LAYOUT settings (feed density,\nfeed layout — a native client lays out its own way), the per-event `notify_*`\ntoggles (delivery-side, inert for a client), and `agent_enabled` (surfaced\nthrough the Ask AI plumbing).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Configuration retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "config"
                  ],
                  "properties": {
                    "config": {
                      "type": "object",
                      "required": [
                        "submit_audience",
                        "can_submit_idea",
                        "idea_reviewers",
                        "scoring_enabled",
                        "reviewer_names_visible",
                        "vote_icon",
                        "accent",
                        "vote_change_allowed",
                        "close_date_enabled",
                        "attachments_enabled",
                        "ai_assist_enabled",
                        "campaigns_enabled",
                        "campaign_creators",
                        "can_create_campaign",
                        "can_assign_review_panel",
                        "campaign_reviewers",
                        "campaign_options",
                        "stages"
                      ],
                      "properties": {
                        "submit_audience": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/IdeasAudience"
                            }
                          ],
                          "description": "Who can submit ideas — everyone, or one group."
                        },
                        "can_submit_idea": {
                          "type": "boolean",
                          "description": "Whether THIS caller may submit an idea right now, under the audience above. Authoritative — drive the Submit affordance off this, not off `submit_audience.type`.",
                          "example": true
                        },
                        "idea_reviewers": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/IdeasReviewPanel"
                            }
                          ],
                          "description": "The workspace review panel every idea routes to unless its campaign overrides it. `source` is `configured` or `fallback`."
                        },
                        "scoring_enabled": {
                          "type": "boolean",
                          "description": "RICE scoring (score pills, the impact/effort matrix, the \"Highest Score on Top\" sort). Default true.",
                          "example": true
                        },
                        "reviewer_names_visible": {
                          "type": "boolean",
                          "description": "Whether reviewer identities may be shown. When false, BOTH roster endpoints — `GET /ideas/{idea_id}/reviewers` and `GET /ideas/campaigns/{id}/reviewers` — return 403 `reviewer_names_hidden` — show only the panel's group name and count. This flag does NOT change any shape in this payload (no panel here names anyone either way); it exists so a client hides the roster affordance instead of tapping into the 403.",
                          "example": true
                        },
                        "vote_icon": {
                          "type": "string",
                          "description": "The configured upvote glyph. Always a value — an unset\nsetting falls back to `caret-up`.\n\nThe Settings picker offers `caret-up`, `chevron-up`,\n`arrow-up`, `circle-up` and `thumbs-up`. This field echoes\nwhat is STORED rather than re-validating it (the same rule\nthe web's `vote_icon` helper applies), so treat it as an\nopen string and fall back to `caret-up` for any name you\ndon't recognise.\n",
                          "example": "caret-up"
                        },
                        "accent": {
                          "type": "object",
                          "description": "The workspace accent an admin picked in Settings — the colour\nthat tints buttons, active states and subtle backgrounds on\nevery Ideas screen. Tint your own chrome from these tokens so\nthe native app matches the web.\n\nShipped as the KEY **and** its five colour tokens, from the\none palette the web views render from. Do not hardcode a\npalette keyed off `key` alone: the tokens can be re-tuned\nserver-side, and a client copy would silently drift to a\ndifferent shade than the web.\n\n`key` is whitelist-coerced to the four authored accents, so a\nworkspace whose stored value is blank, retired or hand-edited\nreports `blue` — key and tokens together, never a key you\ncannot resolve to a colour.\n",
                          "required": [
                            "key",
                            "primary",
                            "bright",
                            "hover",
                            "subtle",
                            "subtle_text"
                          ],
                          "properties": {
                            "key": {
                              "type": "string",
                              "enum": [
                                "blue",
                                "forest",
                                "purple",
                                "sunset"
                              ],
                              "example": "forest"
                            },
                            "primary": {
                              "type": "string",
                              "description": "Buttons",
                              "links": null,
                              "the main accented fill.": null,
                              "example": "#2d6a4f"
                            },
                            "bright": {
                              "type": "string",
                              "description": "A lighter variant for emphasis / highlights.",
                              "example": "#1B8751"
                            },
                            "hover": {
                              "type": "string",
                              "description": "The pressed / hovered state of `primary`.",
                              "example": "#235640"
                            },
                            "subtle": {
                              "type": "string",
                              "description": "Tinted background for chips",
                              "pills and active rows.": null,
                              "example": "#d8efe3"
                            },
                            "subtle_text": {
                              "type": "string",
                              "description": "Readable text colour ON a `subtle` background.",
                              "example": "#1f5e44"
                            }
                          }
                        },
                        "vote_change_allowed": {
                          "type": "boolean",
                          "description": "Whether a member may remove their own upvote. When false, `DELETE /ideas/{idea_id}/vote` is refused — hide the un-vote affordance.",
                          "example": true
                        },
                        "close_date_enabled": {
                          "type": "boolean",
                          "description": "Whether ideas may carry a voting close date. The one opt-in setting — default **false**. While false, `voting_closes_on` is never surfaced on an idea.",
                          "example": false
                        },
                        "attachments_enabled": {
                          "type": "boolean",
                          "description": "Whether ideas and comments may carry file attachments. Default true.",
                          "example": true
                        },
                        "ai_assist_enabled": {
                          "type": "boolean",
                          "description": "Whether AI writing assistance (expand rough notes into a draft) is offered on the compose form. Default true.",
                          "example": true
                        },
                        "campaigns_enabled": {
                          "type": "boolean",
                          "description": "Whether idea campaigns exist for this workspace. When false, hide the Campaigns section — `GET /ideas/campaigns` returns 403 `campaigns_disabled` — and `can_create_campaign` is false regardless of the audience.",
                          "example": true
                        },
                        "campaign_creators": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/IdeasAudience"
                            }
                          ],
                          "description": "Who can launch new campaigns — everyone, or one group."
                        },
                        "can_create_campaign": {
                          "type": "boolean",
                          "description": "Whether THIS caller may create a campaign right now — the audience above AND `campaigns_enabled`.",
                          "example": true
                        },
                        "can_assign_review_panel": {
                          "type": "boolean",
                          "description": "Whether THIS caller may choose a campaign's review panel, i.e. whether the panel select in a composer is live for them. The workspace capability (default: Ideas admins only), so a member who may CREATE a campaign commonly may not APPOINT its panel — their campaign inherits `campaign_reviewers`. When this is false, `campaign_options.reviewer_groups` is empty for that reason rather than because there are no groups.",
                          "example": true
                        },
                        "campaign_reviewers": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/IdeasReviewPanel"
                            }
                          ],
                          "description": "The panel a NEW campaign pre-fills with. `source` is `configured`, or `inherited` when it mirrors `idea_reviewers`."
                        },
                        "campaign_options": {
                          "type": "object",
                          "description": "The option data a \"New Campaign\" composer needs, so it can only offer values `POST /ideas/campaigns` accepts — the same sets the web form draws its radio buttons and select from.",
                          "properties": {
                            "icons": {
                              "type": "array",
                              "description": "The authored Font Awesome glyph names (no `fa-` prefix) accepted as `icon`. Anything else is rejected with `422 invalid_icon`.",
                              "items": {
                                "type": "string"
                              },
                              "example": [
                                "bullhorn",
                                "rocket",
                                "lightbulb"
                              ]
                            },
                            "colors": {
                              "type": "array",
                              "description": "The accent palette. `key` is what you send as `color`; the three hex values let a client render the card and its chips without shipping the palette.",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "key": {
                                    "type": "string",
                                    "example": "forest"
                                  },
                                  "hex": {
                                    "type": "string",
                                    "example": "#2d6a4f"
                                  },
                                  "subtle": {
                                    "type": "string",
                                    "example": "#d8efe3"
                                  },
                                  "subtle_text": {
                                    "type": "string",
                                    "example": "#1f5e44"
                                  }
                                }
                              }
                            },
                            "reviewer_groups": {
                              "type": "array",
                              "description": "The groups this workspace can route a campaign to, name ascending — the same list the web form's \"Campaign reviewers\" select offers. Empty while `campaigns_enabled` is false (there is no composer to render then).",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 7
                                  },
                                  "name": {
                                    "type": "string",
                                    "example": "Product Council"
                                  }
                                }
                              }
                            },
                            "default_icon": {
                              "type": "string",
                              "description": "Applied when `icon` is omitted.",
                              "example": "bullhorn"
                            },
                            "default_color": {
                              "type": "string",
                              "description": "Applied when `color` is omitted.",
                              "example": "blue"
                            },
                            "default_reviewer_group_id": {
                              "type": "integer",
                              "nullable": true,
                              "description": "The panel applied when `reviewer_group_id` is omitted entirely (the workspace's configured campaign panel). `null` means a campaign created without one inherits the workspace default panel at read time.",
                              "example": 7
                            }
                          }
                        },
                        "stages": {
                          "type": "array",
                          "description": "The complete lifecycle pipeline, in order (`position` ascending). Never empty. Same stage shape `PATCH /ideas/{idea_id}/stage` reports, so this list can populate a stage picker and its result parsed identically.",
                          "items": {
                            "$ref": "#/components/schemas/IdeaLifecycleStage"
                          }
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Ideas app is not accessible to the caller (error code `access_denied`)."
          }
        }
      }
    },
    "/ideas/ai_draft": {
      "post": {
        "tags": [
          "Ideas"
        ],
        "summary": "Draft an idea description with AI",
        "description": "The native-client mirror of the web \"Draft with AI\" button on the idea\ncompose form (`POST /apps/ideas/list/ai_draft`). Both call the SAME\n`Ideas::AiDraftService#draft_description`, so the two surfaces cannot drift.\nThe campaign-brief equivalent is `POST /api/v1/ideas/campaigns/ai_brief`.\n\n**WRITES NOTHING.** It returns text for the caller to place in the\ndescription field; posting the idea is still `POST /api/v1/ideas`. Safe to\ncall repeatedly, and each call re-drafts.\n\n**Both inputs are optional.** `title` is what the author has typed so far.\nThe notes the author already wrote are read from `description`, falling back\nto `notes` — send either. Whatever was sent is echoed back as `prior_text`,\nso a client can offer \"restore what I had\" after replacing the field with a\ndraft rather than losing the author's own words.\n\n**`fallback` is part of the contract, not decoration.** The drafting service\nnever raises: when the LLM is unavailable it returns a locally-composed\noutline and sets `fallback: true`, with `notice` carrying the \"AI is\nunavailable right now\" wording. Surface `notice` as-is rather than presenting\na fallback outline as a finished AI draft.\n\n**Gating** — two 403s a client should treat differently:\n* `ai_assist_disabled` — the workspace's AI writing-assistance setting is\n  off. Reported by `GET /api/v1/ideas/config` as `ai_assist_enabled: false`,\n  so hide the sparkles control rather than discovering this by tapping.\n* `forbidden` — AI assist is on, but this caller is outside the workspace's\n  submit audience. The same gate `POST /api/v1/ideas` applies, so anyone who\n  cannot post an idea cannot draft one either.\n",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "nullable": true,
                    "description": "The idea title typed so far. Optional.",
                    "example": "Dark mode for the mobile app"
                  },
                  "description": {
                    "type": "string",
                    "nullable": true,
                    "description": "The notes the author has already written, used as the seed for the draft. Takes precedence over `notes`.",
                    "example": "hard to read on the floor at night"
                  },
                  "notes": {
                    "type": "string",
                    "nullable": true,
                    "description": "Alias for `description`, read only when `description` is absent or blank. Present because the web form posts this name."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A draft description. Nothing was created or modified.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "draft",
                    "prior_text"
                  ],
                  "properties": {
                    "draft": {
                      "type": "object",
                      "required": [
                        "text",
                        "fallback",
                        "notice"
                      ],
                      "properties": {
                        "text": {
                          "type": "string",
                          "description": "The drafted description, for the description field.",
                          "example": "On night shift the screen is hard to read…"
                        },
                        "fallback": {
                          "type": "boolean",
                          "description": "True when the LLM was unavailable and `text` is a locally composed outline rather than an AI draft. Badge it.",
                          "example": false
                        },
                        "notice": {
                          "type": "string",
                          "description": "The message to show alongside the draft. Differs by `fallback` — do not hardcode either wording client-side.",
                          "example": "Drafted with AI — edit freely before posting."
                        }
                      }
                    },
                    "prior_text": {
                      "type": "string",
                      "description": "The notes that were sent in, echoed back so a client can offer to restore them. Empty string when nothing was sent.",
                      "example": "hard to read on the floor at night"
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "AI writing assistance is off for the workspace (error code `ai_assist_disabled`), the caller is outside the submit audience (`forbidden`), or the Ideas app is not accessible to them (`access_denied`)."
          }
        }
      }
    },
    "/ideas/list": {
      "get": {
        "tags": [
          "Ideas"
        ],
        "summary": "Ideas feed (\"All Ideas\")",
        "description": "The native-client mirror of the web \"All Ideas\" feed\n(Apps::Ideas::IdeasController#index). A paginated, filtered, ordered list\nof ideas backed by the SAME query object the web uses\n(::Ideas::FeedListQuery), so the two surfaces can't drift.\n\n**Filter** (`filter`, one flat dimension, default `all`):\n* `all` — every idea in the business.\n* `my`  — ideas the caller authored (no stage sub-filter; the web's\n  per-stage \"My Ideas\" facet is intentionally omitted here).\n* `<stage_id>` — every idea in that lifecycle stage. Use a `stage_id`\n  from `counts.by_stage`. An unknown/foreign value falls back to `all`.\n\n**Sort** (`sort`, default `top`; the second level is always `created_at`\nDESC, with `id` DESC as a stable tiebreak):\n* `top`       — Most Voted on Top (`vote_count` desc)\n* `myvoted`   — My Votes on Top (ideas the caller upvoted first)\n* `new`       — Newest on Top (`created_at` desc)\n* `discussed` — Most Discussed on Top (comment count desc)\n* `score`     — Highest Score on Top (`rice_score` desc). When RICE\n  scoring is turned off for the workspace this degrades to `top`\n  (mirroring the web, which hides the option), and `sort` echoes `top`.\n\n**Counts** are filter-blind — `counts.all`, `counts.my` and one\n`counts.by_stage` row per lifecycle stage (position order, count 0 when\nempty) always reflect the whole business, so the client can badge every\nfilter chip. `meta.total_count`, by contrast, reflects the ACTIVE filter\n(it is what pagination is over).\n\n**Description** — every row carries `description`, the idea's plain-text\nbody, on EVERY filter and sort. It is the same field (same column) that\n`GET /ideas/search` and a campaign's idea list return, so one parser handles\nevery list surface, and it is NOT truncated — a client renders whatever\nsnippet its card design needs. This costs no extra query: the body is a\nplain column on the row the feed already loads.\n\n**Voting close date** — each idea carries `voting_closes_on` (ISO date)\nONLY when the tenant has enabled the voting-close-date feature AND that\nidea has a date set; the key is omitted otherwise. Same gate the web uses\nto show the \"Voting closes …\" pill.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "filter",
            "in": "query",
            "required": false,
            "description": "all (default) | my | <stage_id>. An unknown value falls back to all.",
            "schema": {
              "type": "string",
              "default": "all",
              "example": "all"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "top",
                "myvoted",
                "new",
                "discussed",
                "score"
              ],
              "default": "top"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ideas feed retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ideas",
                    "active_filter",
                    "sort",
                    "counts",
                    "meta"
                  ],
                  "properties": {
                    "active_filter": {
                      "type": "string",
                      "description": "The filter actually applied (echoes an unknown value back as `all`).",
                      "example": "all"
                    },
                    "sort": {
                      "type": "string",
                      "description": "The sort actually applied (`score` echoes `top` when scoring is off).",
                      "example": "top"
                    },
                    "ideas": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "title",
                          "description",
                          "stage",
                          "creator",
                          "comments_count",
                          "created_at",
                          "vote_count",
                          "has_voted"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 91
                          },
                          "title": {
                            "type": "string",
                            "example": "Add SSO for contractors"
                          },
                          "description": {
                            "type": "string",
                            "nullable": true,
                            "description": "The idea's plain-text body, on EVERY filter (`all` / `my` / `<stage_id>`) and every sort — the same field, from the same column, that `GET /ideas/search` and a campaign's idea list return. Not truncated: a client renders its own snippet. Null only for an idea whose body is genuinely empty.",
                            "example": "We should let contractors sign in with SSO."
                          },
                          "stage": {
                            "type": "object",
                            "description": "The idea's current lifecycle stage.",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 3
                              },
                              "name": {
                                "type": "string",
                                "example": "Planned"
                              },
                              "color": {
                                "type": "string",
                                "description": "The stage's whitelist-coerced hex color.",
                                "example": "#2e63b3"
                              }
                            }
                          },
                          "creator": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "nullable": true,
                                "example": 49290
                              },
                              "name": {
                                "type": "string",
                                "nullable": true,
                                "description": "The author's full name.",
                                "example": "Dana Lee"
                              },
                              "photo": {
                                "type": "string",
                                "nullable": true,
                                "description": "Absolute avatar URL (a ui-avatars initial tile when the user has no photo).",
                                "example": "https://officechat.workforce.mangoapps.com/system/photos/49290/thumb.png"
                              }
                            }
                          },
                          "comments_count": {
                            "type": "integer",
                            "description": "Non-deleted comments + replies.",
                            "example": 4
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2026-07-31T06:01:58Z"
                          },
                          "vote_count": {
                            "type": "integer",
                            "description": "Positive votes (up_votes_count counter cache).",
                            "example": 12
                          },
                          "has_voted": {
                            "type": "boolean",
                            "description": "Whether the calling user has upvoted this idea.",
                            "example": true
                          },
                          "rice_score": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Cached RICE score; null when not scored. OMITTED entirely while RICE scoring is off for the workspace.",
                            "example": 42
                          },
                          "rice_score_band": {
                            "type": "object",
                            "description": "The colour band `rice_score` renders as, so a client paints the score pill the same way the web does without hardcoding our ranges. `tier` is the stable key to switch on: `high` (>= 700), `medium` (>= 300), `low` below that, `none` when the idea is awaiting its first score. Travels with `rice_score` and is OMITTED under the same gate (RICE scoring off for the workspace).",
                            "properties": {
                              "tier": {
                                "type": "string",
                                "enum": [
                                  "high",
                                  "medium",
                                  "low",
                                  "none"
                                ],
                                "example": "high"
                              },
                              "label": {
                                "type": "string",
                                "description": "Human label for the band.",
                                "example": "High"
                              },
                              "color": {
                                "type": "string",
                                "description": "Foreground hex the web uses.",
                                "example": "#146c43"
                              },
                              "background": {
                                "type": "string",
                                "description": "Background hex the web uses; `transparent` for the `none` band.",
                                "example": "#d1f0e0"
                              }
                            }
                          },
                          "voting_closes_on": {
                            "type": "string",
                            "format": "date",
                            "description": "The idea's voting close date (ISO `YYYY-MM-DD`). Present ONLY when the tenant has the voting-close-date feature enabled (`enable_close_date`) AND this idea has a date set — the key is OMITTED otherwise (not null). Mirrors the web, which shows the \"Voting closes …\" pill under both conditions.",
                            "example": "2026-08-15"
                          }
                        }
                      }
                    },
                    "counts": {
                      "type": "object",
                      "description": "Filter-blind badge counts for the whole business.",
                      "required": [
                        "all",
                        "my",
                        "by_stage"
                      ],
                      "properties": {
                        "all": {
                          "type": "integer",
                          "description": "All ideas in the business.",
                          "example": 128
                        },
                        "my": {
                          "type": "integer",
                          "description": "Ideas the caller authored.",
                          "example": 7
                        },
                        "by_stage": {
                          "type": "array",
                          "description": "One row per lifecycle stage, in position order (count 0 when empty).",
                          "items": {
                            "type": "object",
                            "required": [
                              "stage_id",
                              "name",
                              "count"
                            ],
                            "properties": {
                              "stage_id": {
                                "type": "integer",
                                "example": 3
                              },
                              "name": {
                                "type": "string",
                                "example": "Planned"
                              },
                              "count": {
                                "type": "integer",
                                "example": 14
                              }
                            }
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination over the ACTIVE filter.",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Rows matching the active filter.",
                          "example": 128
                        },
                        "current_page": {
                          "type": "integer",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "example": 20
                        },
                        "total_pages": {
                          "type": "integer",
                          "example": 7
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Ideas app is not accessible to the caller (error code `access_denied`)."
          }
        }
      }
    },
    "/ideas/search": {
      "get": {
        "tags": [
          "Ideas"
        ],
        "summary": "Search ideas",
        "description": "Free-text search over the Ideas feed — the native-client mirror of the\nweb search box (\"Ideas\" bottom-nav → search → type). Backed by the SAME\nquery object the list + web feed use (`::Ideas::FeedListQuery`, given a\nsearch term), so search and list agree on matching, ordering, counting\nand preloading.\n\n**`q`** is matched case-insensitively against the idea **title** and\n**description** (LIKE wildcards in the term are escaped). A blank/absent\n`q` returns the full feed (browse), matching the web, whose empty search\nbox shows every idea.\n\n**Sort** (`sort`, default `top`; second level always `created_at` DESC,\n`id` DESC tiebreak): `top` (Most Voted) | `myvoted` | `new` | `discussed`\n| `score` (RICE; degrades to `top` and echoes `top` when scoring is off).\n\nEach row is the SAME card the feed serializes PLUS **`description`** (the\nplain-text body). `voting_closes_on` is present ONLY when the tenant has\nthe voting-close-date feature enabled AND that idea has a date set.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Search term (title + description, case-insensitive). Blank returns the full feed.",
            "schema": {
              "type": "string",
              "example": "single sign-on"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "top",
                "myvoted",
                "new",
                "discussed",
                "score"
              ],
              "default": "top"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Search results retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ideas",
                    "query",
                    "sort",
                    "meta"
                  ],
                  "properties": {
                    "query": {
                      "type": "string",
                      "description": "The search term actually applied (echoes `q`, trimmed).",
                      "example": "single sign-on"
                    },
                    "sort": {
                      "type": "string",
                      "description": "The sort actually applied (`score` echoes `top` when scoring is off).",
                      "example": "top"
                    },
                    "ideas": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "title",
                          "description",
                          "stage",
                          "creator",
                          "comments_count",
                          "created_at",
                          "vote_count",
                          "has_voted"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 91
                          },
                          "title": {
                            "type": "string",
                            "example": "Add SSO for contractors"
                          },
                          "description": {
                            "type": "string",
                            "nullable": true,
                            "description": "The idea's plain-text body.",
                            "example": "We should let contractors sign in with SSO."
                          },
                          "stage": {
                            "type": "object",
                            "description": "The idea's current lifecycle stage.",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 3
                              },
                              "name": {
                                "type": "string",
                                "example": "Planned"
                              },
                              "color": {
                                "type": "string",
                                "description": "The stage's whitelist-coerced hex color.",
                                "example": "#2e63b3"
                              }
                            }
                          },
                          "creator": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "nullable": true,
                                "example": 49290
                              },
                              "name": {
                                "type": "string",
                                "nullable": true,
                                "description": "The author's full name.",
                                "example": "Dana Lee"
                              },
                              "photo": {
                                "type": "string",
                                "nullable": true,
                                "description": "Absolute avatar URL (a ui-avatars initial tile when the user has no photo).",
                                "example": "https://officechat.workforce.mangoapps.com/system/photos/49290/thumb.png"
                              }
                            }
                          },
                          "comments_count": {
                            "type": "integer",
                            "description": "Non-deleted comments + replies.",
                            "example": 4
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2026-07-31T06:01:58Z"
                          },
                          "vote_count": {
                            "type": "integer",
                            "description": "Positive votes (up_votes_count counter cache).",
                            "example": 12
                          },
                          "has_voted": {
                            "type": "boolean",
                            "description": "Whether the calling user has upvoted this idea.",
                            "example": true
                          },
                          "rice_score": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Cached RICE score (the idea's score); null when not scored. OMITTED entirely while RICE scoring is off for the workspace.",
                            "example": 42
                          },
                          "rice_score_band": {
                            "type": "object",
                            "description": "The colour band `rice_score` renders as, so a client paints the score pill the same way the web does without hardcoding our ranges. `tier` is the stable key to switch on: `high` (>= 700), `medium` (>= 300), `low` below that, `none` when the idea is awaiting its first score. Travels with `rice_score` and is OMITTED under the same gate (RICE scoring off for the workspace).",
                            "properties": {
                              "tier": {
                                "type": "string",
                                "enum": [
                                  "high",
                                  "medium",
                                  "low",
                                  "none"
                                ],
                                "example": "high"
                              },
                              "label": {
                                "type": "string",
                                "description": "Human label for the band.",
                                "example": "High"
                              },
                              "color": {
                                "type": "string",
                                "description": "Foreground hex the web uses.",
                                "example": "#146c43"
                              },
                              "background": {
                                "type": "string",
                                "description": "Background hex the web uses; `transparent` for the `none` band.",
                                "example": "#d1f0e0"
                              }
                            }
                          },
                          "voting_closes_on": {
                            "type": "string",
                            "format": "date",
                            "description": "The idea's voting close date (ISO `YYYY-MM-DD`). Present ONLY when the tenant has the voting-close-date feature enabled (`enable_close_date`) AND this idea has a date set — the key is OMITTED otherwise (not null).",
                            "example": "2026-08-15"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination over the matching set.",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Ideas matching the search.",
                          "example": 8
                        },
                        "current_page": {
                          "type": "integer",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "example": 20
                        },
                        "total_pages": {
                          "type": "integer",
                          "example": 1
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Ideas app is not accessible to the caller (error code `access_denied`)."
          }
        }
      }
    },
    "/ideas/review_queue": {
      "get": {
        "tags": [
          "Ideas"
        ],
        "summary": "My Review Queue",
        "description": "The native-client mirror of the web \"My Review Queue\"\n(Apps::Ideas::ReviewQueueController#index queue tab): the ideas routed to\na review panel the caller belongs to, ranked by RICE priority. Backed by\nthe SAME query object semantics the web uses (::Ideas::ReviewQueueQuery),\nso the two surfaces can't drift.\n\n**Panel membership is the only grant** — admins get no bypass. A caller on\nno review panel receives `on_panel: false` with an empty list and zeroed\ncounts (HTTP 200, NOT 403), mirroring the web tab's empty state; the client\nshould hide the Review tab for such users (see\n`GET /api/v1/apps?include_navigation=true`, which only exposes the Reviews\nitem to reviewers).\n\n**Campaign filter** (`campaign`, default `all`):\n* `all`  — every idea in the caller's accessible set.\n* `none` — only ideas not attached to any campaign.\n* `<id>` — only that campaign's ideas. Use an `id` from `campaigns`\n  (the campaigns represented in the caller's accessible set). A\n  non-numeric value falls back to `all`.\n\n**Awaiting-score sub-filter** (`needs`): when truthy, only ideas that have\nno RICE score yet (awaiting their first score) are returned. Ignored when\nRICE scoring is turned off for the workspace.\n\n**`needs_scoring_count`** badges the \"awaiting score\" chip: the number of\nunscored ideas WITHIN the active campaign filter. It is measured on the\ncampaign-filtered set (not the `needs`-filtered one), so it stays stable\nwhile the toggle is on. Zero when scoring is off.\n\nOrdering is RICE `rice_score` DESC (unscored ideas sink to the bottom),\nthen `created_at` DESC, then `id` DESC as a stable tiebreak.\n`meta.total_count` reflects the ACTIVE filters (it is what pagination is\nover).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "campaign",
            "in": "query",
            "required": false,
            "description": "all (default) | none (ideas not in any campaign) | <campaign_id>. A non-numeric value falls back to all.",
            "schema": {
              "type": "string",
              "default": "all",
              "example": "none"
            }
          },
          {
            "name": "needs",
            "in": "query",
            "required": false,
            "description": "Only ideas awaiting their first RICE score. Ignored when scoring is disabled.\nDeclared as a boolean, and it now BEHAVES as one: only `1`, `t`, `true` or `on` turns the filter on, and every other value — including the `false` / `0` a generated client emits for an unchecked chip — leaves it off, exactly as omitting the key does. (Before 2026-09-10 the value was read for mere presence, so `needs=false` switched the filter ON and a reviewer who unchecked \"Awaiting score\" watched their already-scored items disappear.) Same true-spelling allowlist as this API's `with_ideas` and `open_for_submissions`.",
            "schema": {
              "type": "boolean",
              "example": true
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Review queue retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "on_panel",
                    "ideas",
                    "active_campaign",
                    "needs_score",
                    "needs_scoring_count",
                    "campaigns",
                    "meta"
                  ],
                  "properties": {
                    "on_panel": {
                      "type": "boolean",
                      "description": "Whether the caller belongs to any review panel. When false, `ideas` is empty and the counts are zero.",
                      "example": true
                    },
                    "active_campaign": {
                      "type": "string",
                      "description": "The campaign filter actually applied (`all` | `none` | `<id>`; echoes an unknown value back as `all`).",
                      "example": "none"
                    },
                    "needs_score": {
                      "type": "boolean",
                      "description": "Whether the awaiting-score sub-filter is active (always false when scoring is off).",
                      "example": false
                    },
                    "needs_scoring_count": {
                      "type": "integer",
                      "description": "Ideas awaiting their first score within the active campaign filter (stable while `needs` is on; 0 when scoring is off).",
                      "example": 8
                    },
                    "ideas": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "display_id",
                          "title",
                          "stage",
                          "creator",
                          "comments_count",
                          "created_at",
                          "vote_count",
                          "has_voted",
                          "description"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 91
                          },
                          "display_id": {
                            "type": "string",
                            "description": "The human reference shown on the web (\"ID-<n>\").",
                            "example": "ID-91"
                          },
                          "title": {
                            "type": "string",
                            "example": "Add SSO for contractors"
                          },
                          "stage": {
                            "type": "object",
                            "description": "The idea's current lifecycle stage.",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 3
                              },
                              "name": {
                                "type": "string",
                                "example": "Planned"
                              },
                              "color": {
                                "type": "string",
                                "description": "The stage's whitelist-coerced hex color.",
                                "example": "#2e63b3"
                              }
                            }
                          },
                          "creator": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "nullable": true,
                                "example": 49290
                              },
                              "name": {
                                "type": "string",
                                "nullable": true,
                                "description": "The author's full name.",
                                "example": "Dana Lee"
                              },
                              "photo": {
                                "type": "string",
                                "nullable": true,
                                "description": "Absolute avatar URL (a ui-avatars initial tile when the user has no photo).",
                                "example": "https://officechat.workforce.mangoapps.com/system/photos/49290/thumb.png"
                              }
                            }
                          },
                          "comments_count": {
                            "type": "integer",
                            "description": "Non-deleted comments + replies.",
                            "example": 4
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2026-07-31T06:01:58Z"
                          },
                          "vote_count": {
                            "type": "integer",
                            "description": "Positive votes (up_votes_count counter cache).",
                            "example": 12
                          },
                          "has_voted": {
                            "type": "boolean",
                            "description": "Whether the calling user has upvoted this idea.",
                            "example": true
                          },
                          "rice_score": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Cached RICE score; null when the idea is awaiting its first score. OMITTED entirely while RICE scoring is off for the workspace.",
                            "example": 42
                          },
                          "rice_score_band": {
                            "type": "object",
                            "description": "The colour band `rice_score` renders as, so a client paints the score pill the same way the web does without hardcoding our ranges. `tier` is the stable key to switch on: `high` (>= 700), `medium` (>= 300), `low` below that, `none` when the idea is awaiting its first score. Travels with `rice_score` and is OMITTED under the same gate (RICE scoring off for the workspace).",
                            "properties": {
                              "tier": {
                                "type": "string",
                                "enum": [
                                  "high",
                                  "medium",
                                  "low",
                                  "none"
                                ],
                                "example": "high"
                              },
                              "label": {
                                "type": "string",
                                "description": "Human label for the band.",
                                "example": "High"
                              },
                              "color": {
                                "type": "string",
                                "description": "Foreground hex the web uses.",
                                "example": "#146c43"
                              },
                              "background": {
                                "type": "string",
                                "description": "Background hex the web uses; `transparent` for the `none` band.",
                                "example": "#d1f0e0"
                              }
                            }
                          },
                          "description": {
                            "type": "string",
                            "nullable": true,
                            "description": "The idea's plain-text description (description_text); null for legacy ideas saved before a description was required.",
                            "example": "Contractors need SSO so we can offboard them centrally."
                          },
                          "voting_closes_on": {
                            "type": "string",
                            "format": "date",
                            "description": "The idea's voting close date (ISO `YYYY-MM-DD`). Present ONLY when the tenant has the voting-close-date feature enabled (`enable_close_date`) AND this idea has a date set — the key is OMITTED otherwise (not null). Mirrors the web, which shows the \"Voting closes …\" pill under both conditions.",
                            "example": "2026-08-15"
                          }
                        }
                      }
                    },
                    "campaigns": {
                      "type": "array",
                      "description": "The campaigns represented in the caller's accessible set — the options for the `campaign` filter.",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "title"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 4
                          },
                          "title": {
                            "type": "string",
                            "example": "Q4 Cost Savings"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination over the ACTIVE filters.",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Rows matching the active filters.",
                          "example": 23
                        },
                        "current_page": {
                          "type": "integer",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "example": 20
                        },
                        "total_pages": {
                          "type": "integer",
                          "example": 2
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Ideas app is not accessible to the caller (error code `access_denied`)."
          }
        }
      }
    },
    "/ideas/campaigns": {
      "get": {
        "tags": [
          "Ideas"
        ],
        "summary": "Idea campaigns list + search",
        "description": "The native-client mirror of the web Campaigns index\n(`Apps::Ideas::CampaignsController#index`). A paginated, phase-filtered list\nof campaigns backed by the SAME query object and phase SQL the web uses\n(`Ideas::CampaignListQuery` + `Campaign.phase_sql`), so the two surfaces\ncan't drift.\n\n**Requires campaigns to be enabled.** The whole endpoint is gated on the\nIdeas `campaigns` setting (on unless an admin turns it off). When it is off\nthe response is `403` with error code `campaigns_disabled` — distinct from\nthe app-access `403 access_denied` — so a client can hide the Campaigns tab\nrather than showing it and failing on tap.\n\n**`status` is the DERIVED phase**, not the stored `status` enum (which only\nhas open/closed and would report a future-dated campaign as \"open\"):\n* `scheduled` — the start date is in the future.\n* `closed` — manually closed, or the close date has passed.\n* `open` — everything else (a campaign with no close date is open-ended).\n\n**Filter** (`filter`, default `all`): `all` | `open` | `scheduled` |\n`closed`. An unknown value falls back to `all`.\n\n**Search** (`q`, optional): a case-insensitive substring matched against the\ncampaign **title OR description (brief)** — the same `Campaign.search`\npredicate the web index uses, so a term returns the same campaigns on both\nsurfaces. A mid-word slice matches (`ffice` finds \"Green Office\"); `%` and\n`_` are matched literally rather than as wildcards. Blank/absent is a no-op.\nThe applied term is echoed back as `query` (trimmed, `\"\"` when not\nsearching) so a client can render \"N campaigns for “term”\".\n\nSearch composes with `filter` and with pagination, and it narrows the\n`counts` as well as the rows — see **Counts** below. A term that matches\nnothing is a normal `200` with an empty `campaigns` array and zeroed\ncounts, never an error.\n\n**Order** is the per-tab order the web uses: Open → closing soonest first\n(open-ended last), Scheduled → opening soonest first, Closed → most recently\nended first, All → title A–Z. Every order ends in a title + id tiebreak, so\npaging never repeats or skips a row.\n\n**Review panel** (`reviewers`, per row) is the panel that campaign's ideas\nroute to: its own group when it names one, else the **workspace default**. It\ncarries the group (`id` + `name`), the panel's FULL member `count`, and up to\n**3** `members` for an avatar stack. Three things worth reading twice:\n\n* `count` is the panel's real size, **not** `members.size` — the preview caps\n  at 3, so a client rendering \"N reviewers\" off the array would say 3 for a\n  40-person panel.\n* `members` is ordered by user id ascending, which makes it exactly the first\n  page of `GET /ideas/campaigns/{id}/reviewers` — the card and the roster it\n  opens cannot disagree. Use that endpoint for the paginated, searchable list.\n* `members` is **omitted entirely** while the `reviewer_names` setting is off\n  (`group` and `count` still ship). Same withholding the campaign detail, the\n  idea detail and the roster endpoint apply, so a tenant that hides reviewer\n  identities hides them here too.\n\nA stale `reviewer_group_id`, or one belonging to another tenant, reads as\n**unset** (`group: null`, `count: 0`) rather than leaking a foreign group name.\n\n**Counts** are filter-blind but search-scoped. `counts.all` / `open` /\n`scheduled` / `closed` let the client badge every chip without re-fetching,\nand `all` is exactly the sum of the three phases. They describe the whole\nbusiness when not searching, and the MATCHING campaigns when `q` is present\n(so each chip badges what tapping it actually returns). `meta.total_count`,\nby contrast, reflects the ACTIVE filter — it is what pagination is over.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "filter",
            "in": "query",
            "required": false,
            "description": "Derived-phase filter. An unknown value falls back to `all`.",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "open",
                "scheduled",
                "closed"
              ],
              "default": "all"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Search term — case-insensitive substring matched against the campaign title OR description (brief). Matches mid-word; `%` and `_` are literal, not wildcards. Blank/absent disables the search. Narrows `counts` as well as the rows, and composes with `filter` + pagination.",
            "schema": {
              "type": "string",
              "example": "savings"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Campaigns retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "campaigns",
                    "active_filter",
                    "query",
                    "counts",
                    "meta"
                  ],
                  "properties": {
                    "active_filter": {
                      "type": "string",
                      "description": "The filter actually applied (echoes an unknown value back as `all`).",
                      "example": "all"
                    },
                    "query": {
                      "type": "string",
                      "description": "The search term actually applied, trimmed. Empty string when not searching. Echoed so a client can label the results (\"N campaigns for “term”\").",
                      "example": "savings"
                    },
                    "campaigns": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "title",
                          "description",
                          "status",
                          "start_date",
                          "end_date",
                          "icon",
                          "color",
                          "color_hex",
                          "has_my_idea",
                          "ideas_count",
                          "reviewers"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 12
                          },
                          "title": {
                            "type": "string",
                            "example": "Q4 Cost Savings"
                          },
                          "description": {
                            "type": "string",
                            "nullable": true,
                            "description": "The campaign brief.",
                            "example": "Ideas to cut operating cost this quarter."
                          },
                          "status": {
                            "type": "string",
                            "description": "The DERIVED phase (see above) — not the stored status enum.",
                            "enum": [
                              "open",
                              "scheduled",
                              "closed"
                            ],
                            "example": "open"
                          },
                          "start_date": {
                            "type": "string",
                            "format": "date",
                            "nullable": true,
                            "description": "ISO-8601 date; null when the campaign has no start date.",
                            "example": "2026-07-21"
                          },
                          "end_date": {
                            "type": "string",
                            "format": "date",
                            "nullable": true,
                            "description": "ISO-8601 date; null when the campaign is open-ended.",
                            "example": "2026-08-20"
                          },
                          "icon": {
                            "type": "string",
                            "description": "Font Awesome glyph name with no `fa-` prefix",
                            "coerced to the authored set (default `bullhorn`).": null,
                            "example": "rocket"
                          },
                          "color": {
                            "type": "string",
                            "description": "Palette key",
                            "whitelist-coerced (default `blue`).": null,
                            "enum": [
                              "blue",
                              "indigo",
                              "purple",
                              "magenta",
                              "red",
                              "sunset",
                              "amber",
                              "forest",
                              "teal",
                              "slate"
                            ],
                            "example": "forest"
                          },
                          "color_hex": {
                            "type": "string",
                            "description": "The palette key's primary hex",
                            "so a client can render without shipping the palette.": null,
                            "example": "#2d6a4f"
                          },
                          "has_my_idea": {
                            "type": "boolean",
                            "description": "Whether the CALLING user has authored at least one idea in this campaign.",
                            "example": true
                          },
                          "ideas_count": {
                            "type": "integer",
                            "description": "Ideas submitted to this campaign.",
                            "example": 2
                          },
                          "reviewers": {
                            "type": "object",
                            "description": "The review panel this campaign's ideas route to — its own group when it names one, else the workspace default panel (the same fallback the web card and the campaign detail apply, so a card can never name a different panel than the detail it opens).",
                            "required": [
                              "group",
                              "count"
                            ],
                            "properties": {
                              "group": {
                                "type": "object",
                                "nullable": true,
                                "description": "`null` (with `count: 0`) when the campaign names no panel AND the workspace has no default — and also when its saved `reviewer_group_id` is stale or belongs to another tenant, which reads as unset rather than leaking the foreign group's name.",
                                "required": [
                                  "id",
                                  "name"
                                ],
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 44387
                                  },
                                  "name": {
                                    "type": "string",
                                    "example": "Product Council"
                                  }
                                }
                              },
                              "count": {
                                "type": "integer",
                                "description": "The panel's FULL member count — **not** `members.size`. The preview is capped at 3, so rendering \"N reviewers\" off the array length would report 3 for a 40-person panel.",
                                "example": 12
                              },
                              "members": {
                                "type": "array",
                                "maxItems": 3,
                                "description": "Up to **3** panel members for an avatar stack, ordered by user id ascending — the same leading slice `GET /ideas/campaigns/{id}/reviewers` returns on page 1, so a card's avatars and the roster screen it opens agree. **OMITTED entirely** while the `reviewer_names` setting is off (`group` and `count` still ship, so a card can say \"Reviewed by <panel> (N)\" without naming anyone). Use the roster endpoint for the paginated, searchable full list.",
                                "items": {
                                  "type": "object",
                                  "required": [
                                    "id",
                                    "name",
                                    "photo"
                                  ],
                                  "properties": {
                                    "id": {
                                      "type": "integer",
                                      "example": 63
                                    },
                                    "name": {
                                      "type": "string",
                                      "nullable": true,
                                      "example": "Grace Dalton"
                                    },
                                    "photo": {
                                      "type": "string",
                                      "description": "Absolute avatar URL; never null (falls back to a generated initials image).",
                                      "example": "https://…/avatar.jpg"
                                    }
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "counts": {
                      "type": "object",
                      "description": "Filter-blind but search-scoped badge counts: the whole business when not searching, the MATCHING campaigns when `q` is present. `all` is the sum of the three phases. All zero when a search matches nothing.",
                      "required": [
                        "all",
                        "open",
                        "scheduled",
                        "closed"
                      ],
                      "properties": {
                        "all": {
                          "type": "integer",
                          "example": 8
                        },
                        "open": {
                          "type": "integer",
                          "example": 3
                        },
                        "scheduled": {
                          "type": "integer",
                          "example": 2
                        },
                        "closed": {
                          "type": "integer",
                          "example": 3
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination over the ACTIVE filter (and search, when `q` is present).",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Rows matching the active filter + search.",
                          "example": 8
                        },
                        "current_page": {
                          "type": "integer",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "example": 20
                        },
                        "total_pages": {
                          "type": "integer",
                          "example": 1
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Ideas app is not accessible to the caller (error code `access_denied`), or campaigns are turned off for the workspace (error code `campaigns_disabled`)."
          }
        }
      },
      "post": {
        "tags": [
          "Ideas"
        ],
        "summary": "Launch an idea campaign",
        "description": "Creates a campaign — the native mirror of the web **+ New Campaign** form\n(`Apps::Ideas::CampaignsController#create`), sharing\n`Ideas::CampaignCreationService` so the two surfaces produce identical rows.\n\nOnly **`title`** and **`brief`** are required. Everything else has the same\ndefault the web form pre-fills, so the minimal request creates exactly what\nthe web would.\n\n**The campaign is created open**, but `status` on every campaign response is\nthe DERIVED phase (`Campaign#phase`), not the stored enum — so a campaign\nposted with a future `start_date` comes back `scheduled`, and one with a past\n`close_date` comes back `closed`. That is the same derivation the list, its\nfilters and its counts use.\n\n**Values a browser form could never get wrong are validated here**, because a\nnative client has no radio buttons or date pickers to constrain it:\n* `icon` / `color` must be from the authored sets (see the enums below). An\n  unknown value is a `422`, not a silent default — storing a glyph the reader\n  would coerce away shows the creator an icon they never picked. Omit either\n  and you get the web form's default (`bullhorn` / `blue`).\n* dates are parsed strictly as ISO-8601 `YYYY-MM-DD` (what the web's\n  `date_field` posts). `01/09/2026` is a `422`, deliberately: reading it\n  leniently would book the campaign in a month the client never stated.\n* `reviewer_group_id` must name a group in **this** business — an id from\n  another tenant is a `422`, never a campaign routed to a foreign panel.\n\n**`reviewer_group_id` has three distinct behaviours**, matching the web form:\n* **omitted** — the workspace's configured campaign panel\n  (`campaign_reviewer_group_id`, reported by `GET /ideas/config` as\n  `campaign_reviewers` / `campaign_options.default_reviewer_group_id`) is\n  applied. This is what the web form pre-selects, so a client that renders no\n  picker still lands where the web would.\n* **sent empty** (`\"\"` or `null`) — no panel is stored, and the campaign\n  resolves to the workspace default panel at read time. This is the web's\n  *\"— Workspace default —\"* option.\n* **an id** — that panel reviews this campaign's ideas.\n\n**Authorization** is the workspace's *\"Who can create campaigns\"* audience\n(`campaign_creators`) — everyone, or only one configured group, with no admin\nbypass. Reading campaigns is not enough (`403 forbidden`). Campaigns being\nturned off for the workspace answers the broader `403 campaigns_disabled`\nfirst, so a client hides the whole tab rather than the one affordance. Check\n`can_create_campaign` on `GET /ideas/config` to decide whether to show the\n\"+\" at all.\n\n**`GET /ideas/config` → `campaign_options`** carries the composer's option\ndata (the icon set, the palette with hex values, and this workspace's\nselectable reviewer groups), so a native New Campaign screen can only offer\nvalues this endpoint accepts.\n\nResponds **201** with the SAME canonical campaign object\n`GET /api/v1/ideas/campaigns/{id}` returns — one query object and one\nserializer back both — so a client can push straight to the detail screen\nwithout a second call. `ideas` is empty and `ideas_count` is `0` on a fresh\ncampaign.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "title",
                  "brief"
                ],
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 120,
                    "description": "Required. Surrounding whitespace is stripped.",
                    "example": "Mobile Experience 2026"
                  },
                  "brief": {
                    "type": "string",
                    "description": "Required — what the campaign is about and what kind of ideas it wants. This is the text every campaign response returns as `description`; that name is accepted here too (as an alias), so a client can post back the object it just read without renaming a key. `brief` wins when both are sent, and an explicitly blank `brief` is a `422` rather than silently falling through to `description`.",
                    "example": "Ideas to make the frontline mobile app faster and simpler."
                  },
                  "description": {
                    "type": "string",
                    "description": "Alias for `brief` (see above). Ignored when `brief` is present."
                  },
                  "icon": {
                    "type": "string",
                    "description": "Optional Font Awesome glyph name with no `fa-` prefix. Defaults to `bullhorn` (the web form's default). An unlisted value is a `422` `invalid_icon`.",
                    "enum": [
                      "bullhorn",
                      "mobile-screen-button",
                      "user-plus",
                      "wand-magic-sparkles",
                      "shield-halved",
                      "rocket",
                      "lightbulb",
                      "bolt",
                      "heart",
                      "leaf",
                      "chart-line",
                      "gear",
                      "calendar-days",
                      "clipboard-check",
                      "comments",
                      "headset",
                      "store",
                      "truck",
                      "users",
                      "star",
                      "flag",
                      "graduation-cap",
                      "globe",
                      "briefcase"
                    ],
                    "default": "bullhorn",
                    "example": "rocket"
                  },
                  "color": {
                    "type": "string",
                    "description": "Optional palette key for the campaign's card and chips. Defaults to `blue`. An unlisted value is a `422` `invalid_color`. The response echoes both the key (`color`) and its primary hex (`color_hex`).",
                    "enum": [
                      "blue",
                      "indigo",
                      "purple",
                      "magenta",
                      "red",
                      "sunset",
                      "amber",
                      "forest",
                      "teal",
                      "slate"
                    ],
                    "default": "blue",
                    "example": "forest"
                  },
                  "start_date": {
                    "type": "string",
                    "format": "date",
                    "nullable": true,
                    "description": "Optional ISO-8601 `YYYY-MM-DD`. Submissions open on this date — a future value makes the campaign `scheduled`. Also accepted as `start_on` (the web's own field name). Anything that isn't a well-formed ISO day is a `422` `invalid_date`.",
                    "example": "2026-09-01"
                  },
                  "close_date": {
                    "type": "string",
                    "format": "date",
                    "nullable": true,
                    "description": "Optional ISO-8601 `YYYY-MM-DD`, and must be on or after `start_date` (else `422 invalid`). Submissions close automatically on this date. Also accepted as `end_date` (the key the response returns it under) and `close_on` (the web's field name). Leave both dates blank for an open-ended campaign.",
                    "example": "2026-09-30"
                  },
                  "reviewer_group_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Optional. The panel that manages the review lifecycle for this campaign's ideas. Omit the key to inherit the workspace's configured campaign panel; send it empty/null to store no panel (resolving to the workspace default at read time); send an id from `GET /ideas/config` → `campaign_options.reviewer_groups`. An id outside this business is a `422` `invalid_reviewer_group`.",
                    "example": 7
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Campaign created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "campaign"
                  ],
                  "properties": {
                    "campaign": {
                      "type": "object",
                      "description": "The created campaign, in the SAME shape `GET /api/v1/ideas/campaigns/{id}` returns — see that endpoint for the full field list. `ideas` is `[]`, `ideas_count` is `0` and `has_my_idea` is `false` on a fresh campaign.",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 31
                        },
                        "title": {
                          "type": "string",
                          "example": "Mobile Experience 2026"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "description": "The campaign brief."
                        },
                        "status": {
                          "type": "string",
                          "description": "The DERIVED phase, not the stored enum.",
                          "enum": [
                            "open",
                            "scheduled",
                            "closed"
                          ],
                          "example": "open"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "example": "2026-09-01"
                        },
                        "end_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "example": "2026-09-30"
                        },
                        "icon": {
                          "type": "string",
                          "example": "rocket"
                        },
                        "color": {
                          "type": "string",
                          "example": "forest"
                        },
                        "color_hex": {
                          "type": "string",
                          "example": "#2d6a4f"
                        },
                        "ideas_count": {
                          "type": "integer",
                          "example": 0
                        },
                        "has_my_idea": {
                          "type": "boolean",
                          "example": false
                        },
                        "creator": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 4
                            },
                            "name": {
                              "type": "string",
                              "example": "Ada Adminson"
                            },
                            "photo": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "reviewers": {
                          "type": "object",
                          "description": "The panel this campaign routes to (its own group, else the workspace default): its `group.name`, the panel's FULL `count`, and an avatar preview of at most 3 `members`. `members` is withheld while the \"Show reviewer names\" setting is off.",
                          "properties": {
                            "group": {
                              "type": "object",
                              "nullable": true,
                              "properties": {
                                "id": {
                                  "type": "integer",
                                  "example": 7
                                },
                                "name": {
                                  "type": "string",
                                  "example": "Product Council"
                                }
                              }
                            },
                            "count": {
                              "type": "integer",
                              "description": "The panel's full size",
                              "not the preview length.": null,
                              "example": 8
                            },
                            "members": {
                              "type": "array",
                              "maxItems": 3,
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 63
                                  },
                                  "name": {
                                    "type": "string",
                                    "nullable": true,
                                    "example": "Grace Dalton"
                                  },
                                  "photo": {
                                    "type": "string",
                                    "example": "https://…/avatar.jpg"
                                  }
                                }
                              }
                            }
                          }
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "Whether the CALLER may manage the campaign they just created — always `true` here (they are its creator), and the same flag `GET /ideas/campaigns/{id}` reports.",
                          "example": true
                        },
                        "can_delete": {
                          "type": "boolean",
                          "description": "Always equal to `can_edit` (one manage right).",
                          "example": true
                        },
                        "ideas": {
                          "type": "array",
                          "description": "Always empty on create.",
                          "items": {
                            "type": "object"
                          }
                        },
                        "ideas_meta": {
                          "type": "object",
                          "properties": {
                            "total_count": {
                              "type": "integer",
                              "example": 0
                            },
                            "current_page": {
                              "type": "integer",
                              "example": 1
                            },
                            "per_page": {
                              "type": "integer",
                              "example": 50
                            },
                            "total_pages": {
                              "type": "integer",
                              "example": 0
                            },
                            "sort": {
                              "type": "string",
                              "example": "top"
                            }
                          }
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Campaigns are turned off for the workspace (error code `campaigns_disabled`, checked first), the caller is outside the `campaign_creators` audience (`forbidden`), or the Ideas app is not accessible to them (`access_denied`)."
          },
          "422": {
            "description": "The campaign was not created. `error.code` says why, and `error.message` is the sentence to show: `invalid` — a missing/blank title or brief, a title over 120 characters, or a close date before the start date; `invalid_icon` / `invalid_color` — a value outside the authored set (the message lists the valid ones); `invalid_date` — a date that isn't a well-formed ISO-8601 `YYYY-MM-DD`; `invalid_reviewer_group` — a group id that doesn't belong to this workspace; `create_failed` — the write itself failed; retry. Also `content_blocked` when the workspace's content-moderation policy refuses the submitted text; the message is the policy's own and is safe to show the author verbatim."
          }
        }
      }
    },
    "/ideas/campaigns/ai_brief": {
      "post": {
        "tags": [
          "Ideas"
        ],
        "summary": "Draft a campaign brief with AI",
        "description": "The native mirror of the web's **\"Draft with AI\"** sparkles button beside the\nBrief field on the New/Edit Campaign form\n(`Apps::Ideas::CampaignsController#ai_brief`). Both surfaces call\n`Ideas::AiDraftService#draft_campaign_brief`.\n\n**Writes nothing and launches no campaign.** It returns text for the Brief\nfield; launching is still `POST /ideas/campaigns`. Safe to call repeatedly —\neach call re-drafts.\n\n**`title` is optional and may be blank.** The web button carries\n`formnovalidate` so an author can ask for a brief before committing to a\ntitle, and the service drafts about \"this topic\" when it gets nothing. A blank\ntitle is a `200` with usable text, never a `422`.\n\nSee `POST /ideas/ai_draft` for why `fallback` and `notice` are part of the\ncontract rather than decoration — the shape is identical.\n\n**Access** — the workspace `ai_assist` setting must be on (`403`\n`ai_assist_disabled`), campaigns must be enabled for the workspace (`403`\n`campaigns_disabled`), and the caller must be able to reach a campaign FORM on\nthe web: the `campaign_creators` audience (the New screen) **or** an Ideas\nadmin — the Edit screen's `require_campaign_manager` admits an admin who sits\noutside that audience, so gating on the audience alone would 403 them out of\nthe button their own Edit screen renders. `GET /ideas/config` reports\n`ai_assist`, `campaigns_enabled` and `can_manage_campaigns` up front.\n",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "description": "The campaign title typed so far. Optional; may be blank.",
                    "example": "Q4 Cost Savings"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A draft. Nothing was written.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "draft": {
                      "type": "object",
                      "properties": {
                        "text": {
                          "type": "string",
                          "description": "The drafted brief, ready to place in the Brief field."
                        },
                        "fallback": {
                          "type": "boolean",
                          "description": "True when the LLM was unavailable and this is the LOCAL outline. Badge it differently — do not present it as \"Drafted with AI\".",
                          "example": false
                        },
                        "notice": {
                          "type": "string",
                          "description": "The banner copy for whichever case applied — the web's exact string.",
                          "example": "Drafted with AI — edit anything before launching."
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Ideas app is not accessible to the caller (error code `access_denied`), campaigns are turned off for the workspace (error code `campaigns_disabled`), AI writing assistance is turned off (error code `ai_assist_disabled`), or the caller may not author a campaign (error code `forbidden`)."
          }
        }
      }
    },
    "/ideas/campaigns/lite": {
      "get": {
        "tags": [
          "Ideas"
        ],
        "summary": "Idea campaigns picker (lite)",
        "description": "The stripped-down twin of `GET /ideas/campaigns`: just `{ id, name }` per\ncampaign. Built for a campaign filter/dropdown, where the full card payload is\nwasted bytes.\n\n**`with_ideas` chooses the set** (default `true`):\n\n* `with_ideas=true` (or the parameter omitted) — only campaigns that hold\n  **at least one idea**. Use this for a FILTER over existing ideas (the feed\n  filter, the review queue), where an empty campaign is a dead option that\n  would return nothing. A campaign appears the moment its first idea is\n  submitted and drops out when its last idea is deleted or detached.\n* `with_ideas=false` — **every** campaign in the workspace, empty ones\n  included. Use this for a DESTINATION picker (submitting an idea to a\n  campaign, moving one), because a campaign is empty precisely until someone\n  submits the first idea to it — filtering empties out would hide the campaign\n  an admin just launched.\n\n`true` is the default because it is what this endpoint returned before the\nparameter existed, so a client that sends nothing sees no change. Only the\nstandard false spellings (`false`, `0`, `f`, `off`) turn it off; any other\nvalue — including a blank `?with_ideas=` — reads as `true`, so a malformed\nrequest never silently widens the list. The applied value is echoed back as\n`with_ideas`.\n\n**`open_for_submissions` restricts to submittable campaigns** (default\n`false`):\n\n* `open_for_submissions=false` (or the parameter omitted) — **every phase**:\n  open, scheduled and closed alike. A closed campaign's ideas still exist and\n  are still a legitimate thing to filter to, so a FILTER must be offered it.\n* `open_for_submissions=true` — only campaigns an idea can actually be\n  submitted to. This is the native mirror of the **web \"Post an Idea\" campaign\n  dropdown**: both surfaces read the same\n  `::Ideas::Campaign.open_for_submissions` scope, so they cannot offer a\n  different option set for the same workspace. It excludes a manually closed\n  campaign, a **scheduled** one (its start date is still in the future) and one\n  whose **close date has passed** — note the last two are still `status: open`\n  on the campaign list, and all three are rejected on submission with\n  `campaign is not open for submissions` (`422`), so offering them in a\n  submission picker offers a guaranteed failure. A campaign starting today or\n  closing today is still open.\n\nDefault `false` for backwards compatibility. Symmetrically to `with_ideas`,\nonly the standard true spellings (`true`, `1`, `t`, `on`) turn it on; any other\nvalue — including a blank `?open_for_submissions=` — reads as `false`, so a\nmalformed request never silently *narrows* the list. The applied value is\nechoed back as `open_for_submissions`.\n\n> ⚠️ **`open_for_submissions=true` flips the `with_ideas` default to `false`.**\n> A campaign holds no ideas precisely until someone submits the first one to\n> it, so intersecting both defaults would hide the campaign an admin just\n> launched — exactly the failure the `with_ideas` note above warns about. Send\n> `with_ideas` explicitly to override this in either direction; every\n> combination stays reachable, and both applied values come back in the\n> response.\n\n* `name` is the campaign **title** (the same string `title` carries on the\n  full list).\n* **Unpaginated by design** — a picker needs the whole option set in one\n  call, so there is no `meta` envelope and no `page` / `per_page` parameter.\n  Note that `with_ideas=false` therefore returns every campaign the workspace\n  has ever created, which in a long-lived tenant is a larger payload than the\n  filtered list.\n* Ordered by name, A–Z (case-insensitive), with an id tiebreak so the list is\n  stable across calls. Identical in every mode.\n\n**Requires campaigns to be enabled**, exactly like the full list: when the\nIdeas `campaigns` setting is off the response is `403` with error code\n`campaigns_disabled`, distinct from the app-access `403 access_denied`.\n",
        "parameters": [
          {
            "name": "with_ideas",
            "in": "query",
            "required": false,
            "description": "`true` (default) returns only campaigns holding at least one idea; `false` returns every campaign. Recognised false values are `false`, `0`, `f` and `off` — anything else, including a blank value, is treated as `true`. The default becomes `false` when `open_for_submissions=true`; send this parameter explicitly to override that.",
            "schema": {
              "type": "boolean",
              "default": true
            }
          },
          {
            "name": "open_for_submissions",
            "in": "query",
            "required": false,
            "description": "`true` returns only campaigns an idea can actually be submitted to — the same set the web \"Post an Idea\" dropdown offers, excluding manually closed, scheduled (future start date) and past-close-date campaigns. `false` (default) returns every phase. Recognised true values are `true`, `1`, `t` and `on` — anything else, including a blank value, is treated as `false`. Setting it to `true` also flips the `with_ideas` default to `false`.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Picker list retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "campaigns",
                    "with_ideas",
                    "open_for_submissions"
                  ],
                  "properties": {
                    "campaigns": {
                      "type": "array",
                      "description": "The matching campaigns, name-ascending — narrowed to those holding at least one idea when `with_ideas` is true, and to those open for submissions when `open_for_submissions` is true. Empty when the workspace has nothing to offer.",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "name"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 12
                          },
                          "name": {
                            "type": "string",
                            "description": "The campaign title.",
                            "example": "Q4 Cost Savings"
                          }
                        }
                      }
                    },
                    "with_ideas": {
                      "type": "boolean",
                      "description": "The emptiness filter actually applied — echoed so a client can confirm which set it received. True when the parameter was omitted, blank or unrecognised, EXCEPT under `open_for_submissions=true`, where the default is false.",
                      "example": true
                    },
                    "open_for_submissions": {
                      "type": "boolean",
                      "description": "The phase filter actually applied — echoed for the same reason. False when the parameter was omitted, blank or unrecognised.",
                      "example": false
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Ideas app is not accessible to the caller (error code `access_denied`), or campaigns are turned off for the workspace (error code `campaigns_disabled`)."
          }
        }
      }
    },
    "/ideas/campaigns/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The campaign id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Ideas"
        ],
        "summary": "One campaign's detail",
        "description": "A single campaign with its creator, its review panel, and a PAGINATED page of\nits ideas. The native mirror of the web campaign detail\n(`Apps::Ideas::CampaignsController#show`), backed by the shared\n`::Ideas::CampaignDetailQuery` — so the idea ordering, the reviewer-panel\nfallback and the totals cannot drift between surfaces.\n\n**Requires campaigns to be enabled**, like the rest of this section: when the\nIdeas `campaigns` setting is off the response is `403` with error code\n`campaigns_disabled` (distinct from the app-access `403 access_denied`).\n\nThe campaign's own fields are shaped by the SAME serializer the campaigns\nLIST uses, so a card and its detail always agree: `status` is the DERIVED\nphase (`open` / `scheduled` / `closed` — never the stored open/closed enum),\n`icon` and `color` are whitelist-coerced, and the dates are ISO-8601.\n\n**The ideas list is paginated** (`page` / `per_page`, default 50 — the web's\npage size — clamped to 50). A campaign's thread is unbounded, so an\nunpaginated nested list would be a latent timeout. Crucially, `ideas_count`\nand `ideas_meta.total_count` are computed on the UNPAGINATED scope: they\nreport the campaign's TRUE size, not the size of the page you asked for.\n\n**Sort** (`sort`, default `top`) mirrors the web's options:\n* `top` — most upvoted first (the default)\n* `new` — newest first\n* `discussed` — most comments (replies included) first\n* `score` — highest RICE first, unscored last. Degrades to `top` when RICE\n  scoring is off for the workspace, exactly as the web does. An unknown value\n  degrades to `top` too. The applied value is echoed as `ideas_meta.sort`.\n\nEvery sort ends in an `id` DESC tiebreak, so paging never repeats or skips an\nidea — all four sort keys are non-unique (votes, a comment count, a nullable\nRICE score), and ties are common in a young campaign.\n\n**`reviewers`** resolves the panel the campaign routes to: its own group when\nit names one, else the workspace default — the same fallback the web detail\nand the list card apply. It carries three things:\n* `group.name` — the panel's name, for \"Reviewed by <panel>\". `group` is\n  `null` (with `count: 0`) only when the campaign names no panel AND the\n  workspace has no default.\n* `count` — the panel's **full** member count, whatever the preview length.\n  Never read this off `members.size`; that would report 3 for a 40-person\n  panel.\n* `members` — an avatar preview of at most **3** people, id-ordered. The same\n  three-member preview the idea detail ships (`reviewers` /\n  `reviewers_count`), so both screens render one avatar stack with a \"+N\"\n  overflow computed from `count`. For the complete, paginated, searchable\n  roster of THIS campaign's panel use\n  `GET /ideas/campaigns/{id}/reviewers` — the campaign twin of\n  `GET /ideas/{idea_id}/reviewers`, and the one to call here: a campaign\n  screen has no `idea_id` in scope.\n\n`members` is **omitted entirely** while the `reviewer_names` setting is off,\nwith `group` and `count` still present so a client can render\n\"Reviewed by <panel> (N)\" without naming anyone.\n\nEach idea row carries `rice_score` — and `rice_score_band`, the colour the\nweb paints it — only while RICE scoring is on, matching the feed, search and\nidea-detail endpoints.\n",
        "parameters": [
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Ideas ordering. Unknown values (and `score` while scoring is off) fall back to `top`.",
            "schema": {
              "type": "string",
              "enum": [
                "top",
                "new",
                "discussed",
                "score"
              ],
              "default": "top"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based page over the IDEAS list. An out-of-range page returns an empty list.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Ideas per page (default 50, clamped to 1..50).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 50
            }
          }
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Campaign detail retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "campaign"
                  ],
                  "properties": {
                    "campaign": {
                      "type": "object",
                      "required": [
                        "id",
                        "title",
                        "description",
                        "status",
                        "start_date",
                        "end_date",
                        "icon",
                        "color",
                        "color_hex",
                        "has_my_idea",
                        "ideas_count",
                        "creator",
                        "reviewers",
                        "can_edit",
                        "can_delete",
                        "ideas",
                        "ideas_meta"
                      ],
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 12
                        },
                        "title": {
                          "type": "string",
                          "example": "Q4 Cost Savings"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "description": "The campaign brief.",
                          "example": "Ideas to cut operating cost this quarter."
                        },
                        "status": {
                          "type": "string",
                          "description": "The DERIVED phase — not the stored status enum.",
                          "enum": [
                            "open",
                            "scheduled",
                            "closed"
                          ],
                          "example": "open"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "description": "ISO-8601; null when the campaign has no start date.",
                          "example": "2026-07-21"
                        },
                        "end_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "description": "ISO-8601; null when the campaign is open-ended.",
                          "example": "2026-08-20"
                        },
                        "icon": {
                          "type": "string",
                          "description": "Font Awesome glyph name with no `fa-` prefix",
                          "coerced to the authored set (default `bullhorn`).": null,
                          "example": "rocket"
                        },
                        "color": {
                          "type": "string",
                          "description": "Palette key",
                          "whitelist-coerced (default `blue`).": null,
                          "example": "forest"
                        },
                        "color_hex": {
                          "type": "string",
                          "description": "The palette key's primary hex",
                          "so a client can render without shipping the palette.": null,
                          "example": "#2d6a4f"
                        },
                        "has_my_idea": {
                          "type": "boolean",
                          "description": "Whether the CALLING user has authored at least one idea in this campaign.",
                          "example": true
                        },
                        "ideas_count": {
                          "type": "integer",
                          "description": "The campaign's TRUE idea count, from the unpaginated scope — unaffected by `per_page`.",
                          "example": 14
                        },
                        "creator": {
                          "type": "object",
                          "description": "The person who launched the campaign.",
                          "required": [
                            "id",
                            "name",
                            "photo"
                          ],
                          "properties": {
                            "id": {
                              "type": "integer",
                              "nullable": true,
                              "example": 1
                            },
                            "name": {
                              "type": "string",
                              "nullable": true,
                              "example": "Anup K"
                            },
                            "photo": {
                              "type": "string",
                              "description": "Absolute avatar URL; never null (falls back to a generated initials image).",
                              "example": "https://…/avatar.jpg"
                            }
                          }
                        },
                        "reviewers": {
                          "type": "object",
                          "required": [
                            "group",
                            "count"
                          ],
                          "properties": {
                            "group": {
                              "type": "object",
                              "nullable": true,
                              "description": "The review panel this campaign routes to (its own group, else the workspace default). Null only when neither is configured.",
                              "required": [
                                "id",
                                "name"
                              ],
                              "properties": {
                                "id": {
                                  "type": "integer",
                                  "example": 44387
                                },
                                "name": {
                                  "type": "string",
                                  "example": "Product Council"
                                }
                              }
                            },
                            "count": {
                              "type": "integer",
                              "description": "The panel's FULL member count (0 when there is no panel) — independent of how many `members` are previewed, so use it for \"N reviewers\" and for the avatar stack's \"+N\" overflow.",
                              "example": 8
                            },
                            "members": {
                              "type": "array",
                              "maxItems": 3,
                              "description": "An avatar preview of at most 3 panel members, id-ordered — the same preview length the idea detail ships. OMITTED entirely while the `reviewer_names` setting is off. Use `GET /ideas/campaigns/{id}/reviewers` for the paginated, searchable roster of this campaign's panel.",
                              "items": {
                                "type": "object",
                                "required": [
                                  "id",
                                  "name",
                                  "photo"
                                ],
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 63
                                  },
                                  "name": {
                                    "type": "string",
                                    "nullable": true,
                                    "example": "Grace Dalton"
                                  },
                                  "photo": {
                                    "type": "string",
                                    "example": "https://…/avatar.jpg"
                                  }
                                }
                              }
                            }
                          }
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "Whether the CALLING user may manage this campaign — its creator, or an Ideas admin (business admin-or-above, or the Ideas app-admin role). Managing a campaign is ONE right, as it is on the web, so this same predicate gates `PATCH /ideas/campaigns/{id}` and `PATCH /ideas/campaigns/{id}/reopen`. Render the Edit and Reopen affordances on it and they can never 403 on tap.",
                          "example": true
                        },
                        "can_delete": {
                          "type": "boolean",
                          "description": "Whether the CALLING user may delete this campaign — the exact predicate `DELETE /ideas/campaigns/{id}` applies. Always equal to `can_edit` (one manage right); shipped as its own key because Delete is a separately-rendered affordance.",
                          "example": true
                        },
                        "ideas": {
                          "type": "array",
                          "description": "One page of the campaign's ideas, in the requested sort.",
                          "items": {
                            "type": "object",
                            "required": [
                              "id",
                              "title",
                              "description",
                              "stage",
                              "creator",
                              "comments_count",
                              "created_at",
                              "vote_count",
                              "has_voted"
                            ],
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 87
                              },
                              "title": {
                                "type": "string",
                                "example": "Dark mode for the mobile app"
                              },
                              "description": {
                                "type": "string",
                                "nullable": true,
                                "description": "The idea's plain-text body.",
                                "example": "Ship a dark theme for the native app."
                              },
                              "stage": {
                                "type": "object",
                                "required": [
                                  "id",
                                  "name",
                                  "color"
                                ],
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 2
                                  },
                                  "name": {
                                    "type": "string",
                                    "example": "Reviewing"
                                  },
                                  "color": {
                                    "type": "string",
                                    "example": "#6c757d"
                                  }
                                }
                              },
                              "creator": {
                                "type": "object",
                                "required": [
                                  "id",
                                  "name",
                                  "photo"
                                ],
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "nullable": true,
                                    "example": 63
                                  },
                                  "name": {
                                    "type": "string",
                                    "nullable": true,
                                    "example": "Grace Dalton"
                                  },
                                  "photo": {
                                    "type": "string",
                                    "example": "https://…/avatar.jpg"
                                  }
                                }
                              },
                              "comments_count": {
                                "type": "integer",
                                "description": "Comments + replies (non-deleted).",
                                "example": 3
                              },
                              "created_at": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2026-07-31T09:26:59Z"
                              },
                              "vote_count": {
                                "type": "integer",
                                "description": "Positive-vote total.",
                                "example": 12
                              },
                              "has_voted": {
                                "type": "boolean",
                                "description": "Whether the CALLING user upvoted this idea.",
                                "example": true
                              },
                              "rice_score": {
                                "type": "integer",
                                "nullable": true,
                                "description": "Cached RICE score; null when not scored. OMITTED entirely while RICE scoring is off for the workspace.",
                                "example": 42
                              },
                              "rice_score_band": {
                                "type": "object",
                                "description": "The colour band `rice_score` renders as, so a client paints the score pill the same way the web does without hardcoding our ranges. `tier` is the stable key to switch on: `high` (>= 700), `medium` (>= 300), `low` below that, `none` when the idea is awaiting its first score. Travels with `rice_score` and is OMITTED under the same gate (RICE scoring off for the workspace).",
                                "properties": {
                                  "tier": {
                                    "type": "string",
                                    "enum": [
                                      "high",
                                      "medium",
                                      "low",
                                      "none"
                                    ],
                                    "example": "high"
                                  },
                                  "label": {
                                    "type": "string",
                                    "description": "Human label for the band.",
                                    "example": "High"
                                  },
                                  "color": {
                                    "type": "string",
                                    "description": "Foreground hex the web uses.",
                                    "example": "#146c43"
                                  },
                                  "background": {
                                    "type": "string",
                                    "description": "Background hex the web uses; `transparent` for the `none` band.",
                                    "example": "#d1f0e0"
                                  }
                                }
                              },
                              "voting_closes_on": {
                                "type": "string",
                                "format": "date",
                                "description": "Present only when the voting-close-date feature is on AND this idea has a date set.",
                                "example": "2026-08-15"
                              }
                            }
                          }
                        },
                        "ideas_meta": {
                          "type": "object",
                          "description": "Pagination over the ideas list, plus the sort actually applied.",
                          "required": [
                            "total_count",
                            "current_page",
                            "per_page",
                            "total_pages",
                            "sort"
                          ],
                          "properties": {
                            "total_count": {
                              "type": "integer",
                              "description": "The campaign's true idea count (unpaginated).",
                              "example": 14
                            },
                            "current_page": {
                              "type": "integer",
                              "example": 1
                            },
                            "per_page": {
                              "type": "integer",
                              "example": 50
                            },
                            "total_pages": {
                              "type": "integer",
                              "description": "0 when the campaign has no ideas.",
                              "example": 1
                            },
                            "sort": {
                              "type": "string",
                              "enum": [
                                "top",
                                "new",
                                "discussed",
                                "score"
                              ],
                              "example": "top"
                            }
                          }
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Ideas app is not accessible to the caller (error code `access_denied`), or campaigns are turned off for the workspace (error code `campaigns_disabled`)."
          },
          "404": {
            "description": "No campaign with that id in the caller's business (error code `not_found`)."
          }
        }
      },
      "patch": {
        "tags": [
          "Ideas"
        ],
        "summary": "Edit a campaign",
        "description": "Edits a campaign — the native mirror of the web\n`Apps::Ideas::CampaignsController#update` (**Manage ▸ Edit** on the campaign\ndetail, which posts exactly this field set). Values are normalised and validated\nby the SAME shared code the create endpoint uses\n(`::Ideas::CampaignAttributes`), so a payload `POST /ideas/campaigns` accepts is\naccepted here too — and rejected the same way.\n\n**CREATOR OR IDEAS ADMIN.** The campaign's creator may edit it, and so may an\nIdeas admin (a business admin-or-above, or a user holding the Ideas app-admin\nrole) — the web's `require_campaign_manager` rule verbatim. The admin half is\ndeliberate: creator-only stranded campaigns whose creator had been deactivated\nor had left, with nobody able to fix the drive. Sitting on the campaign's\n**review panel grants nothing here** — reviewing ideas and managing the drive\nthat collects them are different rights. Anyone else gets `403 forbidden`. The\nsame predicate backs **`can_edit`** on the campaign detail, so a client can\nrender the Edit affordance exactly when this endpoint would accept it.\n\n**Requires campaigns to be enabled**, like the rest of this section: with the\nIdeas `campaigns` setting off the response is `403 campaigns_disabled`.\n\n### PATCH semantics — this is the part to read\n\nEvery field is **optional**, and a key you do **not** send is left\n**unchanged**. The web always posts its whole form, so sending everything\nbehaves identically — but an \"edit title\" screen must not be able to blank the\nbrief or silently re-route the review panel just by omitting them. For the\nclearable fields, sending the key **empty** (`\"\"` or `null`) is a distinct,\nmeaningful instruction:\n\n| field | omitted | sent EMPTY | sent with a value |\n|---|---|---|---|\n| `title` | unchanged | `422` — can't be blank | replaces it (whitespace stripped, max 120) |\n| `brief` | unchanged | `422` — can't be blank | replaces it (stripped) |\n| `icon` | unchanged | resets to `bullhorn` | must be an authored glyph, else `422 invalid_icon` |\n| `color` | unchanged | resets to `blue` | must be a palette key, else `422 invalid_color` |\n| `start_date` | unchanged | **cleared** | strict ISO-8601 `YYYY-MM-DD`, else `422 invalid_date` |\n| `close_date` | unchanged | **cleared** → open-ended | strict ISO-8601, on or after the start date |\n| `reviewer_group_id` | unchanged | **cleared** → the workspace default panel | a group in THIS business, else `422 invalid_reviewer_group` |\n\nAn empty **date** clears it because that is exactly what the web does — its\nemptied `date_field` posts `\"\"`, which is how a manager makes a campaign\nopen-ended again. An empty **brief** is refused even when the stored brief is\nalready blank (a campaign predating the field), because the model only validates\na brief it sees change and the card body would silently render empty.\n\n`status` is deliberately **not writable** here: closing and reopening are\nseparate actions on both surfaces (`PATCH /ideas/campaigns/{id}/reopen`), so a\nPATCH fixing a typo can never reopen a finished drive. Sending `status` is\nignored, not an error.\n\n**All-or-nothing:** the first unusable value is returned and **nothing** is\nwritten, so a rejected edit never lands half of itself.\n\nResponds with the SAME canonical campaign object\n`GET /ideas/campaigns/{id}` returns (one query object, one serializer — a client\ncan drop it straight into the screen it saved from), plus `changed` and\n`changed_fields`. A payload matching what is already stored is a **200 no-op**\nwith `changed: false` rather than an error, so a retried or double-tapped save is\nsafe. `sort` / `page` / `per_page` apply to the nested `ideas` list exactly as\nthey do on the detail GET.\n",
        "parameters": [
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Ordering of the nested ideas list, as on the detail GET.",
            "schema": {
              "type": "string",
              "enum": [
                "top",
                "new",
                "discussed",
                "score"
              ],
              "default": "top"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based page over the nested IDEAS list.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Ideas per page in the response (default 50, clamped to 1..50).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 50
            }
          }
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "description": "Send ONLY the fields you are changing. An empty string clears a date or the reviewer group, and resets the icon/colour to the form default.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 120,
                    "description": "Whitespace is stripped. Blank is a 422.",
                    "example": "Q4 Cost Savings"
                  },
                  "brief": {
                    "type": "string",
                    "description": "The campaign's description — the text its cards and detail header render. Blank is a 422.",
                    "example": "Ideas to cut operating cost this quarter."
                  },
                  "description": {
                    "type": "string",
                    "description": "Alias for `brief` — the key every campaign GET returns it under, so a client can PATCH back the object it just read. `brief` wins when both are sent."
                  },
                  "icon": {
                    "type": "string",
                    "description": "Font Awesome glyph name with NO `fa-` prefix, from the authored set (`GET /ideas/config` → `campaign_options.icons`). Empty resets to `bullhorn`.",
                    "example": "lightbulb"
                  },
                  "color": {
                    "type": "string",
                    "description": "Palette key (`GET /ideas/config` → `campaign_options.colors`). Empty resets to `blue`.",
                    "example": "teal"
                  },
                  "start_date": {
                    "type": "string",
                    "format": "date",
                    "nullable": true,
                    "description": "ISO-8601 `YYYY-MM-DD`. Send empty to clear it. Alias `start_on`.",
                    "example": "2026-09-01"
                  },
                  "close_date": {
                    "type": "string",
                    "format": "date",
                    "nullable": true,
                    "description": "ISO-8601 `YYYY-MM-DD`, on or after `start_date` (including a start date already stored). Send empty to clear it — the campaign becomes open-ended. Aliases `end_date`, `close_on`.",
                    "example": "2026-09-30"
                  },
                  "reviewer_group_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "The panel that reviews this campaign's ideas — a group in THIS business (`GET /ideas/config` → `campaign_options.reviewer_groups`). Send empty to store no panel, which resolves to the workspace default at read time. OMIT it and the campaign's current panel is untouched.",
                    "example": 44387
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Campaign updated (or a no-op when the payload matched what was stored — check `changed`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "campaign",
                    "changed",
                    "changed_fields"
                  ],
                  "properties": {
                    "campaign": {
                      "type": "object",
                      "description": "The updated campaign — the SAME object (same serializer, same keys, same nested `ideas` page) that `GET /ideas/campaigns/{id}` returns; see that operation for the full field-by-field shape.",
                      "required": [
                        "id",
                        "title",
                        "description",
                        "status",
                        "start_date",
                        "end_date",
                        "icon",
                        "color",
                        "color_hex",
                        "has_my_idea",
                        "ideas_count",
                        "creator",
                        "reviewers",
                        "can_edit",
                        "can_delete",
                        "ideas",
                        "ideas_meta"
                      ],
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 12
                        },
                        "title": {
                          "type": "string",
                          "example": "Q4 Cost Savings"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "description": "The campaign brief.",
                          "example": "Ideas to cut operating cost this quarter."
                        },
                        "status": {
                          "type": "string",
                          "description": "The DERIVED phase, not the stored enum — a campaign whose start date you just moved into the future reads `scheduled`.",
                          "enum": [
                            "open",
                            "scheduled",
                            "closed"
                          ],
                          "example": "open"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "description": "Null after you clear it.",
                          "example": "2026-09-01"
                        },
                        "end_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "description": "Null when the campaign is open-ended.",
                          "example": "2026-09-30"
                        },
                        "icon": {
                          "type": "string",
                          "example": "lightbulb"
                        },
                        "color": {
                          "type": "string",
                          "example": "teal"
                        },
                        "color_hex": {
                          "type": "string",
                          "description": "The palette key's primary hex.",
                          "example": "#0a97b0"
                        },
                        "has_my_idea": {
                          "type": "boolean",
                          "example": true
                        },
                        "ideas_count": {
                          "type": "integer",
                          "description": "The campaign's TRUE idea count (unpaginated).",
                          "example": 14
                        },
                        "creator": {
                          "type": "object",
                          "description": "{ id, name, photo } — the person who launched it."
                        },
                        "reviewers": {
                          "type": "object",
                          "description": "{ group, count, members? } — the panel it routes to after the edit."
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "Whether the CALLER may edit/reopen it — the same manage predicate this endpoint gates on.",
                          "example": true
                        },
                        "can_delete": {
                          "type": "boolean",
                          "description": "Whether the CALLER may delete it. Always equal to `can_edit` (one manage right).",
                          "example": true
                        },
                        "ideas": {
                          "type": "array",
                          "description": "One page of the campaign's ideas.",
                          "items": {
                            "type": "object"
                          }
                        },
                        "ideas_meta": {
                          "type": "object",
                          "description": "{ total_count, current_page, per_page, total_pages, sort }."
                        }
                      }
                    },
                    "changed": {
                      "type": "boolean",
                      "description": "Whether anything actually moved. False on a no-op save.",
                      "example": true
                    },
                    "changed_fields": {
                      "type": "array",
                      "description": "The API field names that changed — `title`, `brief`, `icon`, `color`, `start_date`, `close_date`, `reviewer_group_id`. Empty on a no-op.",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "title",
                        "close_date"
                      ]
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Ideas app is not accessible (`access_denied`), campaigns are turned off for the workspace (`campaigns_disabled`), or the caller is neither the campaign's creator nor an Ideas admin (`forbidden`)."
          },
          "404": {
            "description": "No campaign with that id in the caller's business (error code `not_found`)."
          },
          "422": {
            "description": "The edit was refused and NOTHING was written. `error.code` says which value: `invalid` (the model — blank title, over-length title, blank brief, close date before start date), `invalid_icon`, `invalid_color`, `invalid_date`, `invalid_reviewer_group`, or `update_failed` (infrastructure). Also `content_blocked` when the workspace's content-moderation policy refuses the submitted text; the message is the policy's own and is safe to show the author verbatim.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Ideas"
        ],
        "summary": "Delete a campaign",
        "description": "Deletes a campaign — the native mirror of the web\n`Apps::Ideas::CampaignsController#destroy` (the **Delete** action on the\ncampaign detail page). Both surfaces run the same\n`::Ideas::CampaignDeletionService`, so neither can leave state the other\nwouldn't.\n\n**CREATOR OR IDEAS ADMIN.** The campaign's creator may delete it, and so may\nan Ideas admin — a business admin-or-above, or a user holding the Ideas\napp-admin role. This is the web's `require_campaign_manager` rule verbatim,\nand it is deliberately **wider than the author-only idea delete**\n(`DELETE /ideas/{id}`): a creator-only rule stranded campaigns whose creator\nhad been deactivated or had left the business, with nobody able to edit,\nclose, reopen or delete them. Anyone else gets `403 forbidden` and the\ncampaign is untouched. Note that contributing an idea to a campaign grants\nnothing here, and neither does the *\"Who can create campaigns\"* audience —\nthat setting governs creating, not deleting.\n\nCheck **`can_delete`** on `GET /ideas/campaigns/{id}` to decide whether to\nshow the affordance: it is the same predicate this endpoint enforces.\n\n**The campaign's IDEAS SURVIVE.** A campaign is a grouping, not an owner: its\nideas are detached (`campaign_id` set to `NULL`) and keep everything they had\n— their votes, comments, RICE scores, attachments, audit trail and lifecycle\nstage. They simply reappear in the feed with no campaign. Nothing else in the\nschema references a campaign, so there is no further cascade.\n`ideas_detached` reports how many moved, so a client can confirm the scope it\nwarned the user about.\n\nThe whole thing runs inside ONE transaction in a constant number of queries —\nthe detach is a single set-based `UPDATE`, so deleting a campaign holding\n1,000 ideas costs what deleting an empty one does. A failure rolls everything\nback: the campaign survives AND its ideas are still attached.\n\n**Requires campaigns to be enabled**, like every action in this section: when\nthe Ideas `campaigns` setting is off the response is `403` with error code\n`campaigns_disabled` (distinct from the app-access `403 access_denied`), and\nit is reported ahead of the narrower `forbidden` so a client hides the whole\nCampaigns tab rather than one button.\n\n**Not idempotent.** A second call returns `404 not_found`, because the\ncampaign is genuinely gone. Clients should treat `404` on a retry as success.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Campaign deleted; its ideas were kept and detached.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "campaign_id",
                    "deleted",
                    "ideas_detached",
                    "message"
                  ],
                  "properties": {
                    "campaign_id": {
                      "type": "integer",
                      "description": "The id of the campaign that was deleted.",
                      "example": 12
                    },
                    "deleted": {
                      "type": "boolean",
                      "description": "Always true on a 200.",
                      "example": true
                    },
                    "ideas_detached": {
                      "type": "integer",
                      "description": "How many ideas left the campaign and are now uncategorised. The ideas themselves are NOT deleted.",
                      "example": 14
                    },
                    "message": {
                      "type": "string",
                      "example": "Campaign deleted. Its ideas were kept and detached."
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Ideas app is not accessible to the caller (error code `access_denied`), campaigns are turned off for the workspace (error code `campaigns_disabled`), or the caller is neither the campaign's creator nor an Ideas admin (error code `forbidden`). The campaign is left untouched."
          },
          "404": {
            "description": "No campaign with that id in the caller's business (error code `not_found`). A campaign belonging to another tenant reports `404`, never `403`, so the response can't confirm that the id exists elsewhere. A non-numeric id 404s at the router."
          },
          "422": {
            "description": "The delete could not be completed and was rolled back (error code `delete_failed`); the campaign survives and its ideas are still attached to it."
          }
        }
      }
    },
    "/ideas/campaigns/{id}/reopen": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The campaign to reopen.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "patch": {
        "tags": [
          "Ideas"
        ],
        "summary": "Reopen a closed campaign for submissions",
        "description": "Reopens a CLOSED campaign so members can post ideas to it again — the native\nmirror of the web \"Reopen campaign\" action (`Apps::Ideas::CampaignsController#reopen`,\nthe Manage menu on the campaign detail, which offers Reopen exactly when the\ncampaign's phase is `closed`). Both surfaces call the same\n`::Ideas::Campaign#reopen`, so neither can drift from the other.\n\n**One reopen writes two things:**\n\n1. the manual `closed` flag is cleared;\n2. a close date that has **already passed** is dropped. Without this second\n   part a date-expired campaign would fall straight back to `closed` the\n   moment it was reopened. A close date still in the FUTURE is left alone.\n\nWhich happened is reported as `close_date_cleared`, because a client whose card\nis showing an end date needs to know the campaign no longer has one.\n\n**Authorization: the campaign's CREATOR, or an Ideas ADMIN** (a business\nadmin-or-above, or a user holding the Ideas app-admin role) — the web's\n`require_campaign_manager` rule verbatim. Anyone else gets `403 forbidden`.\nThe admin half is deliberate: creator-only stranded campaigns whose creator\nhad been deactivated or had left, with nobody able to reopen them. Sitting on\nthe campaign's **review panel grants nothing here** — reviewing ideas and\nmanaging the drive that collects them are different rights.\n\nManaging rights are reported on the campaign detail (`can_edit` on\n`GET /ideas/campaigns/{id}` is the same creator-or-admin predicate this gate\napplies), so a client can offer Reopen exactly when `status` is `closed` AND\nthe caller holds that right, rather than tapping into a 403.\n\n**A campaign that isn't closed is a `200` no-op** (`changed: false`) that\nwrites nothing — never an error — so a retried or double-tapped reopen is\nsafe. Key off `changed`, not the status code. `message` says which no-op it\nwas: \"already open for submissions\" for an open campaign, and when submissions\nactually open for a SCHEDULED one (whose submissions genuinely haven't opened\nyet).\n\n**A reopened campaign whose START date is still ahead comes back as\n`status: \"scheduled\"`, not `\"open\"`** — `status` is the derived phase\n(`Campaign#phase`), and its submissions legitimately open on that date. Read\nit back rather than assuming `\"open\"`.\n\nThere is no request body: the campaign is identified by the path, and reopening\ntakes no options. The response carries the campaign in the **same row shape the\ncampaigns LIST returns**, so a client can drop it straight into the card or\nheader it just acted on. Nothing about the campaign's ideas, review panel or\nroster changes when it reopens, so the heavier detail payload is not used —\ncall `GET /ideas/campaigns/{id}` if the full detail is wanted.\n\nGated on the `campaigns` setting like every campaign endpoint: a workspace with\ncampaigns turned off answers `403 campaigns_disabled`, exactly as the web\nredirects the whole section away.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The campaign is open (or scheduled to open). `changed` distinguishes a reopen that just happened from a no-op on a campaign that wasn't closed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "campaign",
                    "changed",
                    "close_date_cleared",
                    "message"
                  ],
                  "properties": {
                    "changed": {
                      "type": "boolean",
                      "description": "`true` when this call reopened the campaign. `false` when it was not closed and nothing was written.",
                      "example": true
                    },
                    "close_date_cleared": {
                      "type": "boolean",
                      "description": "`true` when the campaign's close date had already passed and was therefore dropped (`end_date` is now `null`). `false` for a future close date, which is kept, and for a no-op.",
                      "example": false
                    },
                    "message": {
                      "type": "string",
                      "description": "Human-readable outcome — `Campaign reopened for submissions.`, or on a no-op why nothing changed.",
                      "example": "Campaign reopened for submissions."
                    },
                    "campaign": {
                      "type": "object",
                      "description": "The campaign in the same row shape `GET /ideas/campaigns` returns.",
                      "required": [
                        "id",
                        "title",
                        "description",
                        "status",
                        "start_date",
                        "end_date",
                        "icon",
                        "color",
                        "color_hex",
                        "has_my_idea",
                        "ideas_count"
                      ],
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 12
                        },
                        "title": {
                          "type": "string",
                          "example": "Q4 Cost Savings"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "description": "The campaign brief.",
                          "example": "Ideas to cut operating cost this quarter."
                        },
                        "status": {
                          "type": "string",
                          "description": "The DERIVED phase after the reopen — `scheduled` when the start date is still ahead, otherwise `open`. Never the stored status enum.",
                          "enum": [
                            "open",
                            "scheduled",
                            "closed"
                          ],
                          "example": "open"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "description": "ISO-8601; null when the campaign has no start date.",
                          "example": "2026-07-21"
                        },
                        "end_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "description": "ISO-8601; `null` when the campaign is open-ended — including when this call just cleared an expired close date (see `close_date_cleared`).",
                          "example": "2026-08-20"
                        },
                        "icon": {
                          "type": "string",
                          "description": "Font Awesome glyph name with no `fa-` prefix",
                          "coerced to the authored set (default `bullhorn`).": null,
                          "example": "rocket"
                        },
                        "color": {
                          "type": "string",
                          "description": "Palette key",
                          "whitelist-coerced (default `blue`).": null,
                          "example": "forest"
                        },
                        "color_hex": {
                          "type": "string",
                          "description": "The palette key's primary hex",
                          "so a client can render without shipping the palette.": null,
                          "example": "#2d6a4f"
                        },
                        "has_my_idea": {
                          "type": "boolean",
                          "description": "Whether the CALLING user has authored at least one idea in this campaign.",
                          "example": true
                        },
                        "ideas_count": {
                          "type": "integer",
                          "description": "Ideas submitted to this campaign — unchanged by a reopen.",
                          "example": 14
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Ideas app is not accessible (`access_denied`), campaigns are turned off for the workspace (`campaigns_disabled`), or the caller is neither the campaign's creator nor an Ideas admin (`forbidden`). Review-panel members are NOT exempt."
          },
          "404": {
            "description": "No campaign with that id in the caller's business (`not_found`). A non-numeric id 404s at the router."
          },
          "422": {
            "description": "`reopen_failed` — the campaign could not be saved (the message carries the model's own validation sentence). Nothing was written."
          }
        }
      }
    },
    "/ideas/campaigns/{id}/close": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The campaign to close.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "patch": {
        "tags": [
          "Ideas"
        ],
        "summary": "Close a campaign to new submissions",
        "description": "Closes a campaign so members can no longer post ideas to it — the native mirror\nof the web \"Close campaign\" action (`Apps::Ideas::CampaignsController#close`, the\nManage menu on the campaign detail, which offers Close exactly when the\ncampaign's phase is NOT `closed`). The twin of\n`PATCH /ideas/campaigns/{id}/reopen`; both surfaces call the same\n`::Ideas::Campaign#close`, so a close is identical wherever it comes from.\n\n**Its ideas survive, untouched.** Closing pauses NEW submissions only: every\nidea already collected stays open for voting, comments, RICE scoring and stage\nmoves, and keeps appearing in the feed. This is **not** a delete and **not** an\narchive — use `DELETE /ideas/campaigns/{id}` if you mean to remove the campaign\n(which detaches its ideas rather than deleting them).\n\n**`end_date` is deliberately left alone.** A campaign closed EARLY keeps the\nclose date it advertised; only the manual closed flag is written. The campaigns\nlist reads the record's `updated_at` (which this call touches) as the end\ntimestamp for exactly that case, so nothing needs the date rewritten — and\nrewriting it would silently change what every card says about the campaign.\n\n**Authorization: the campaign's CREATOR, or an Ideas ADMIN** (a business\nadmin-or-above, or a user holding the Ideas app-admin role) — the web's\n`require_campaign_manager` rule verbatim. Anyone else gets `403 forbidden`.\nSitting on the campaign's **review panel grants nothing** — reviewing ideas and\nmanaging the drive that collects them are different rights.\n\n**A SCHEDULED campaign can be closed** (the web offers Close for it too) — that\nis cancelling a drive before it ever opens. Its `start_date` is not rewritten,\nand it comes back `status: \"closed\"`.\n\n**An already-closed campaign is a `200` no-op** (`changed: false`) that writes\nnothing, so a retried or double-tapped close is safe. Key off `changed`, not the\nstatus code. `message` distinguishes the two ways a campaign is already closed,\nbecause they are different facts: closed by hand (\"This campaign is already\nclosed.\") versus closed by its close date passing (\"This campaign already closed\non <date>.\"). In the second case the stored status stays `open` — writing the\nflag would change nothing a client can see.\n\nThere is no request body: the campaign is identified by the path, and closing\ntakes no options. The response carries the campaign in the **same row shape the\ncampaigns LIST returns**, review panel included, so a client can drop it straight\ninto the card or header it just acted on.\n\nGated on the `campaigns` setting like every campaign endpoint: a workspace with\ncampaigns turned off answers `403 campaigns_disabled`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The campaign is closed. `changed` distinguishes a close that just happened from a no-op on a campaign that was already closed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "campaign",
                    "changed",
                    "message"
                  ],
                  "properties": {
                    "changed": {
                      "type": "boolean",
                      "description": "`true` when this call closed the campaign. `false` when it was already closed and nothing was written.",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "description": "Human-readable outcome — `Campaign closed.`, or on a no-op which kind of already-closed it was.",
                      "example": "Campaign closed."
                    },
                    "campaign": {
                      "type": "object",
                      "description": "The campaign in the same row shape `GET /ideas/campaigns` returns.",
                      "required": [
                        "id",
                        "title",
                        "description",
                        "status",
                        "start_date",
                        "end_date",
                        "icon",
                        "color",
                        "color_hex",
                        "has_my_idea",
                        "ideas_count",
                        "reviewers"
                      ],
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 12
                        },
                        "title": {
                          "type": "string",
                          "example": "Q4 Cost Savings"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "description": "The campaign brief.",
                          "example": "Ideas to cut operating cost this quarter."
                        },
                        "status": {
                          "type": "string",
                          "description": "The DERIVED phase after the close — `closed`, including for a campaign that was still `scheduled` beforehand.",
                          "enum": [
                            "open",
                            "scheduled",
                            "closed"
                          ],
                          "example": "closed"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "description": "ISO-8601; never rewritten by a close.",
                          "example": "2026-07-21"
                        },
                        "end_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "description": "ISO-8601, and **unchanged by this call** — a campaign closed early keeps the close date it advertised. `null` only when the campaign was already open-ended.",
                          "example": "2026-08-20"
                        },
                        "icon": {
                          "type": "string",
                          "description": "Font Awesome glyph name with no `fa-` prefix",
                          "coerced to the authored set (default `bullhorn`).": null,
                          "example": "rocket"
                        },
                        "color": {
                          "type": "string",
                          "description": "Palette key",
                          "whitelist-coerced (default `blue`).": null,
                          "example": "forest"
                        },
                        "color_hex": {
                          "type": "string",
                          "description": "The palette key's primary hex",
                          "so a client can render without shipping the palette.": null,
                          "example": "#2d6a4f"
                        },
                        "has_my_idea": {
                          "type": "boolean",
                          "description": "Whether the CALLING user has authored at least one idea in this campaign.",
                          "example": true
                        },
                        "ideas_count": {
                          "type": "integer",
                          "description": "Ideas submitted to this campaign — unchanged by a close; they are not deleted or detached.",
                          "example": 14
                        },
                        "reviewers": {
                          "type": "object",
                          "description": "The campaign's review panel, in the same `{ group, count, members? }` shape the list and the detail report. `members` is capped at 3 and omitted entirely while the `reviewer_names` setting is off.",
                          "required": [
                            "group",
                            "count"
                          ],
                          "properties": {
                            "group": {
                              "type": "object",
                              "nullable": true,
                              "required": [
                                "id",
                                "name"
                              ],
                              "properties": {
                                "id": {
                                  "type": "integer",
                                  "example": 44387
                                },
                                "name": {
                                  "type": "string",
                                  "example": "Product Council"
                                }
                              }
                            },
                            "count": {
                              "type": "integer",
                              "description": "The panel's FULL member count",
                              "not `members.size`.": null,
                              "example": 12
                            },
                            "members": {
                              "type": "array",
                              "maxItems": 3,
                              "items": {
                                "type": "object",
                                "required": [
                                  "id",
                                  "name",
                                  "photo"
                                ],
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 63
                                  },
                                  "name": {
                                    "type": "string",
                                    "nullable": true,
                                    "example": "Grace Dalton"
                                  },
                                  "photo": {
                                    "type": "string",
                                    "example": "https://…/avatar.jpg"
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Ideas app is not accessible (`access_denied`), campaigns are turned off for the workspace (`campaigns_disabled`), or the caller is neither the campaign's creator nor an Ideas admin (`forbidden`). Review-panel members are NOT exempt."
          },
          "404": {
            "description": "No campaign with that id in the caller's business (`not_found`). A non-numeric id 404s at the router."
          },
          "422": {
            "description": "`close_failed` — the campaign could not be saved (the message carries the model's own validation sentence). Nothing was written."
          }
        }
      }
    },
    "/ideas/campaigns/{id}/reviewers": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The campaign id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Ideas"
        ],
        "summary": "A campaign's reviewer roster",
        "description": "The people who review this campaign's ideas — the members of the panel the\ncampaign routes to: **its own panel when it names one, else the workspace\ndefault**. That is the identical resolution the web campaign detail\n(`Apps::Ideas::CampaignsController#show`), the campaigns index card and\n`Ideas::CampaignDetailQuery` apply, so this roster can never disagree with\nthem about whose panel it is.\n\nThis is the **full-list twin** of the capped `reviewers.members` avatar\npreview on `GET /ideas/campaigns/{id}` — exactly the relationship\n`GET /ideas/{idea_id}/reviewers` has to the idea detail. Render the stack\nfrom the detail, open this for the complete, filterable list. The two agree\nby construction: same panel, same order, and the detail's `reviewers.count`\nequals this endpoint's `meta.total_count`.\n\n**Ordering** is `users.id` ascending — the order the web campaign detail\nrenders its roster in (its `where(id: member_ids)` carries no `ORDER BY`),\nand deterministic, so paging never skips or repeats a reviewer.\n\n**Search** (`q`) matches the term case-insensitively against each reviewer's\nname (name / first / last) and email — the server-side equivalent of the\nweb panel's client-side filter. `%` and `_` are matched literally, and\n`meta.total_count` narrows to the matches while the `panel` node does not.\n\n**Access** is every member who can open the campaign: seeing who reviews it\nis not a manage right, matching the web detail, which shows the panel to\nevery reader. Three gates apply, in this order:\n* the Ideas app must be accessible (`403 access_denied`);\n* campaigns must be enabled (`403 campaigns_disabled`) — the whole section,\n  like every campaign endpoint;\n* the `reviewer_names` setting must be ON (`403 reviewer_names_hidden`), the\n  same gate the idea roster carries. It is checked BEFORE the campaign\n  lookup, so a workspace that hides names answers the same 403 for a\n  campaign that does not exist — the gate cannot be used to probe for ids.\n\n`panel` names the group the roster belongs to (title the screen \"Reviewed by\n<panel>\"), or `null` when the campaign names no panel AND the workspace has\nno default — then `reviewers` is empty and `meta.total_count` is 0. A\n`reviewer_group_id` pointing at another tenant's group resolves to `null`\nthe same way: panel lookup is business-scoped, so it can never roster\nsomebody else's people.\n\n**Query budget: constant** — one cached membership lookup, one COUNT, and one\npage of users with the whole avatar-variant chain preloaded. Neither the\npanel's size nor `per_page` adds a query.\n",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Free-text search over reviewer name / email.",
            "schema": {
              "type": "string"
            },
            "example": "alice"
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based page number (default 1). A page past the end is an empty 200.",
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "example": 1
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Rows per page (default 20, clamped to 50).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            },
            "example": 20
          }
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Campaign reviewer roster retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "campaign_id",
                    "reviewers",
                    "panel",
                    "meta"
                  ],
                  "properties": {
                    "campaign_id": {
                      "type": "integer",
                      "description": "The campaign the roster belongs to, echoed so a client can match the response to the card it opened.",
                      "example": 12
                    },
                    "reviewers": {
                      "type": "array",
                      "description": "The panel members, `users.id`-ordered, one page at a time.",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "name",
                          "photo"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer",
                            "description": "The reviewer's user id.",
                            "example": 334
                          },
                          "name": {
                            "type": "string",
                            "description": "Display name (full_name",
                            "falling back to name).": null,
                            "example": "Alice Anderson"
                          },
                          "photo": {
                            "type": "string",
                            "nullable": true,
                            "description": "Absolute avatar URL (request host prepended to a relative path; ui-avatars fallback otherwise).",
                            "example": "https://ui-avatars.com/api/?name=Alice%20Anderson&size=40&background=random"
                          }
                        }
                      }
                    },
                    "panel": {
                      "type": "object",
                      "nullable": true,
                      "description": "The review panel (NotificationRecipientGroup) the roster belongs to — the campaign's own group, else the workspace default. Null when neither is configured (or the stored group belongs to another tenant), in which case `reviewers` is empty.",
                      "required": [
                        "id",
                        "name"
                      ],
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 44387
                        },
                        "name": {
                          "type": "string",
                          "example": "Campaign Crew"
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "total_count",
                        "current_page",
                        "per_page",
                        "total_pages"
                      ],
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Reviewers matching the active search — the panel's FULL size when `q` is blank, computed on the unpaginated relation, so it is unaffected by `per_page`. Equals the campaign detail's `reviewers.count`.",
                          "example": 8
                        },
                        "current_page": {
                          "type": "integer",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "example": 20
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "0 when the roster is empty.",
                          "example": 1
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Ideas app is not accessible to the caller (`access_denied`), campaigns are turned off for the workspace (`campaigns_disabled`), or reviewer names are hidden (`reviewer_names_hidden` — reported even for an unknown campaign id, since the gate precedes the lookup)."
          },
          "404": {
            "description": "No campaign with that id in the caller's business (`not_found`). A campaign belonging to another tenant reports `404`, never `403`; a non-numeric id 404s at the router."
          }
        }
      }
    },
    "/ideas/{idea_id}/reviewers": {
      "parameters": [
        {
          "name": "idea_id",
          "in": "path",
          "required": true,
          "description": "The idea id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Ideas"
        ],
        "summary": "An idea's reviewer roster",
        "description": "The people who can review this idea — the members of the review panel it\nroutes to (its campaign's panel when the campaign sets one, else the\nworkspace default panel). The native mirror of the web idea detail's\n\"Review Panel\" modal (Apps::Ideas::IdeasController#show), backed by the\nshared `::Ideas::ReviewerRosterQuery` + the same panel resolution the web\nuses, so the roster can't drift between surfaces.\n\n**Ordering** is `users.id` ascending — the same stable order the web modal\nrenders, and deterministic so pagination never skips or repeats a\nreviewer.\n\n**Search** (`q`) matches the term (case-insensitively) against each\nreviewer's name and email — the server-side equivalent of the web modal's\n\"Search reviewers\" box.\n\n**Access** requires the `reviewer_names` setting to be ON. When an admin\nhas turned reviewer names off, the web shows only the panel's group name;\nthis endpoint returns `403 reviewer_names_hidden` so the client can hide\nthe roster affordance rather than tap into an error.\n\n`panel` names the group the roster belongs to (the \"Reviewed by …\" line),\nor `null` when the idea has no panel configured (then `reviewers` is empty\nand `meta.total_count` is 0). `meta.total_count` also drives the\n\"N reviewers\" header.\n",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Free-text search over reviewer name / email.",
            "schema": {
              "type": "string"
            },
            "example": "alice"
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based page number (default 1).",
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "example": 1
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Rows per page (default 20, clamped to 50).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            },
            "example": 20
          }
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Reviewer roster retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "reviewers",
                    "panel",
                    "meta"
                  ],
                  "properties": {
                    "reviewers": {
                      "type": "array",
                      "description": "The panel members, id-ordered, one page at a time.",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "name",
                          "photo"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer",
                            "description": "The reviewer's user id.",
                            "example": 334
                          },
                          "name": {
                            "type": "string",
                            "description": "Display name (full_name",
                            "falling back to name).": null,
                            "example": "Alice Anderson"
                          },
                          "photo": {
                            "type": "string",
                            "nullable": true,
                            "description": "Absolute avatar URL (request host prepended to a relative path; ui-avatars fallback otherwise).",
                            "example": "https://ui-avatars.com/api/?name=Alice%20Anderson&size=40&background=random"
                          }
                        }
                      }
                    },
                    "panel": {
                      "type": "object",
                      "nullable": true,
                      "description": "The review panel (NotificationRecipientGroup) the roster belongs to; null when the idea has no panel configured.",
                      "required": [
                        "id",
                        "name"
                      ],
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 44387
                        },
                        "name": {
                          "type": "string",
                          "example": "Idea Reviewers"
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "total_count",
                        "current_page",
                        "per_page",
                        "total_pages"
                      ],
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total reviewers matching the active search (the whole panel when q is blank).",
                          "example": 8
                        },
                        "current_page": {
                          "type": "integer",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "example": 20
                        },
                        "total_pages": {
                          "type": "integer",
                          "example": 1
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Either the Ideas app is not accessible (`access_denied`), or reviewer names are turned off for the workspace (`reviewer_names_hidden`)."
          },
          "404": {
            "description": "No idea with that id in the caller's business (`not_found`)."
          }
        }
      }
    },
    "/ideas/{idea_id}/voters": {
      "parameters": [
        {
          "name": "idea_id",
          "in": "path",
          "required": true,
          "description": "The idea id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Ideas"
        ],
        "summary": "The users who upvoted an idea",
        "description": "The people who upvoted this idea. The native mirror of the web idea\ndetail's \"See who upvoted\" modal\n(`Apps::Ideas::IdeasController#voters`), backed by the shared\n`::Ideas::VoterListQuery`, so the two surfaces can't drift in ordering, in\nwhat a search term matches, or in who is listed at all.\n\n**POSITIVE votes only.** A row is an `up` vote, plus any legacy `yes`\nfrom the retired thumbs model (which still counts as support). A legacy\n`no` is never listed: there is no down-vote data model and no\ndown-voter list.\n\n**Ordering** is newest vote first (`voted_on` descending), with a\n**`id` descending tiebreak**. The tiebreak is load-bearing, not\ndecoration — votes seeded in bulk share one timestamp, and a tied set has\nno inherent order, so without it a page boundary could repeat or skip a\nvoter the user already scrolled past. Paging is therefore stable across\nrepeated requests.\n\n**Search** (`q`) matches the term as a case-insensitive substring of the\nvoter's name — the server-side equivalent of the web modal's\n\"Search by name…\" box. It matches mid-word, and `%` / `_` are matched\nliterally rather than as SQL wildcards. Blank/absent is a no-op. The\napplied term is echoed back as `query` (trimmed, `\"\"` when not\nsearching). A term that matches nothing is a normal `200` with an empty\n`voters` array — never an error.\n\n**Two different totals, deliberately.** `meta.total_count` is the\nSEARCHED total and is what pagination is over. `total_votes` is the\nidea's UNFILTERED positive-vote total, so a client can keep showing the\nreal \"N votes\" headline while the user filters the list — it matches the\nvote count on the idea card and detail.\n\n`id` on each row is the **USER's** id, not the vote row's: it is what a\nclient needs to open that person's profile, which is what tapping a voter\ndoes in the mobile design. The vote has no client-facing identity here.\n\n`photo` is **never null** — the platform falls back to a generated\ninitials avatar when the user has no profile photo, so a client needs no\nplaceholder branch. `is_you` flags the calling user's own row, driving the\n\"You\" pill next to their name.\n",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Case-insensitive substring match on the voter's name. Matches mid-word; `%` and `_` are literal. Blank/absent disables the search. Narrows `meta.total_count` but NOT `total_votes`.",
            "schema": {
              "type": "string"
            },
            "example": "kejriwal"
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based page number (default 1). An out-of-range page returns an empty list.",
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "example": 1
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Rows per page (default 20, clamped to 1..50).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Voters retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "voters",
                    "query",
                    "idea_id",
                    "total_votes",
                    "meta"
                  ],
                  "properties": {
                    "voters": {
                      "type": "array",
                      "description": "One row per positive vote, newest first.",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "name",
                          "photo",
                          "voted_on",
                          "is_you"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer",
                            "description": "The USER's id (not the vote row's) — use it to open the person's profile.",
                            "example": 455
                          },
                          "name": {
                            "type": "string",
                            "nullable": true,
                            "description": "The voter's full name.",
                            "example": "Amaya Kejriwal"
                          },
                          "photo": {
                            "type": "string",
                            "description": "Absolute avatar URL. Never null — falls back to a generated initials image when the user has no profile photo.",
                            "example": "https://officechat-dev.workforce.mangoapps.com/rails/active_storage/…/avatar.jpg"
                          },
                          "voted_on": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "description": "ISO-8601 timestamp of when this user upvoted.",
                            "example": "2026-07-20T10:00:00Z"
                          },
                          "is_you": {
                            "type": "boolean",
                            "description": "True on the CALLING user's own row — drives the \"You\" pill. At most one row per response.",
                            "example": false
                          }
                        }
                      }
                    },
                    "query": {
                      "type": "string",
                      "description": "The search term actually applied, trimmed. Empty string when not searching.",
                      "example": "kejriwal"
                    },
                    "idea_id": {
                      "type": "integer",
                      "description": "Echo of the idea these voters belong to.",
                      "example": 4
                    },
                    "total_votes": {
                      "type": "integer",
                      "description": "The idea's UNFILTERED positive-vote total — unchanged by `q`, so the \"N votes\" headline stays truthful while filtering. Matches the vote count on the idea card.",
                      "example": 9
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination over the SEARCHED set.",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Voters matching the search.",
                          "example": 9
                        },
                        "current_page": {
                          "type": "integer",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "example": 20
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "0 when nothing matches.",
                          "example": 1
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Ideas app is not accessible to the caller (error code `access_denied`)."
          },
          "404": {
            "description": "No idea with that id in the caller's business (error code `not_found`)."
          }
        }
      }
    },
    "/ideas/{idea_id}/stage": {
      "parameters": [
        {
          "name": "idea_id",
          "in": "path",
          "required": true,
          "description": "The idea to move.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "patch": {
        "tags": [
          "Ideas"
        ],
        "summary": "Move an idea to a lifecycle stage",
        "description": "Moves an idea to another lifecycle stage — the native mirror of the web\nreviewer action (`Apps::Ideas::IdeasController#stage`: the stage picker on\nthe idea detail, and the Kanban board's drag-to-move). Both surfaces, plus\nthe Ideas agent's `move_stage` tool, run the SAME\n`::Ideas::StageTransitionService`, so a move is identical wherever it comes\nfrom.\n\n**One move writes four things, in one transaction** — if any fails, none\nof it sticks (an idea that moved with no audit trail is exactly what the\ntransaction prevents):\n\n1. the idea's stage;\n2. an **audit entry** recording who moved it, from where, to where, and the\n   note — the reviewer accountability trail shown on the idea detail;\n3. the `note`, if given, as a **real comment on the idea's Discussion**, so\n   every member — not just reviewers — can see why it moved;\n4. `declined_from_stage_id`, **only** when the target stage's category is\n   `declined` — the off-ramp the detail page's Lifecycle card reads to say\n   which stage the idea fell out of. It is left untouched by any other move.\n\nA stage-change **notification** is then sent to the idea's author (outside\nthe transaction, and subject to the workspace's `notify_stage_change`\nsetting), so a delivery failure can never undo a committed move.\n\n**Authorization: review-panel membership on THIS idea, with NO admin\nbypass** — the web rule verbatim. The panel is the idea's campaign's\nreviewer group when its campaign sets one, else the workspace default\npanel. An Ideas admin who is not on that panel gets `403`, and so does the\nidea's own author. `GET /ideas/config` reports the panel\n(`idea_reviewers`), and `GET /ideas/{id}` reports the idea's own, so a\nclient can hide the stage picker rather than tapping into a 403.\n\n**Already in the target stage** is a `200` with `changed: false` and\n**nothing written** — no audit entry, no duplicate note comment, no\nnotification. The call is therefore idempotent and safe to retry. (The web\naction answers its Kanban caller with a bare `204` here; this endpoint\nreturns a body because a native client needs the idea's current state\neither way, and every other endpoint on this surface returns one.)\n\nThere is **no stage list here** — `GET /ideas/config` already returns the\nworkspace's ordered pipeline in the same stage shape, which is what\npopulates a stage picker.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "stage_id"
                ],
                "properties": {
                  "stage_id": {
                    "type": "integer",
                    "description": "The target stage. Must be a lifecycle stage of the caller's own business — an unknown id, a non-numeric one, or another tenant's stage is `422 unknown_stage` and moves nothing.",
                    "example": 412
                  },
                  "note": {
                    "type": "string",
                    "description": "Optional rationale. Recorded on the audit entry AND posted to the idea's Discussion as a comment by the caller. Blank/whitespace is treated as absent (nothing is posted).",
                    "example": "Moving to Planned — costed and scheduled for Q4."
                  }
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "stage_id"
                ],
                "properties": {
                  "stage_id": {
                    "type": "integer",
                    "example": 412
                  },
                  "note": {
                    "type": "string",
                    "example": "Moving to Planned."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The idea is in the target stage. `changed` distinguishes a move that just happened from a no-op (it was already there).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "idea_id",
                    "changed",
                    "stage",
                    "previous_stage",
                    "note_posted",
                    "message"
                  ],
                  "properties": {
                    "idea_id": {
                      "type": "integer",
                      "example": 42
                    },
                    "changed": {
                      "type": "boolean",
                      "description": "`true` when this call moved the idea (audit entry written, note posted if given, author notified). `false` when it was already in that stage and nothing was written.",
                      "example": true
                    },
                    "stage": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/IdeaLifecycleStage"
                        }
                      ],
                      "description": "The stage the idea is in now."
                    },
                    "previous_stage": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/IdeaLifecycleStage"
                        }
                      ],
                      "description": "The stage it came from. `null` if the idea's previous stage row no longer exists. On a no-op this is the same stage as `stage`."
                    },
                    "declined_from_stage_id": {
                      "type": "integer",
                      "nullable": true,
                      "description": "The idea's current off-ramp pointer — the stage it was declined FROM, which the detail page's Lifecycle card reads. A move into a `declined` stage sets it; nothing clears it if the idea is later revived, so a previously-deferred idea still reports it after moving back out. `null` for an idea never declined.",
                      "example": null
                    },
                    "note_posted": {
                      "type": "boolean",
                      "description": "Whether a note was actually added to the Discussion.",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "description": "Human-readable outcome, e.g. `Moved to “Planned”.` or `Already in “Planned”.`",
                      "example": "Moved to “Planned”."
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Either the Ideas app is not accessible (`access_denied`), or the caller is not a member of this idea's review panel (`forbidden`). Admins are NOT exempt — panel membership is the only grant."
          },
          "404": {
            "description": "No idea with that id in the caller's business (`not_found`)."
          },
          "422": {
            "description": "`stage_id_required` when it is missing; `unknown_stage` when it is not a lifecycle stage of this business; `stage_change_failed` when the transaction could not be committed (nothing moved)."
          }
        }
      }
    },
    "/ideas/{idea_id}/vote": {
      "parameters": [
        {
          "name": "idea_id",
          "in": "path",
          "required": true,
          "description": "The idea id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Ideas"
        ],
        "summary": "Cast the caller's upvote on an idea",
        "description": "Records the caller's single upvote (Ideas is upvote-only — one vote per\nuser per idea). The native mirror of the web\nApps::Ideas::VotesController#create.\n\n* **Idempotent** — voting again is a no-op that still returns `200` with\n  the current state (the DB is unique on user + idea).\n* Refused with `422 voting_closed` once the idea's voting-close date has\n  passed — only on a workspace whose `close_date_enabled` is true. Where\n  that feature is off the date is inert and the vote is accepted.\n\nReturns the idea's resulting vote state. `vote_count` / `has_voted` use\nthe SAME key names the feed card does, so a client can patch its cached\ncard in place; `vote_change_allowed` tells the client whether to show a\nremove-vote affordance.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Vote recorded (or already present).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdeaVoteState"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Ideas app is not accessible to the caller (error code `access_denied`)."
          },
          "404": {
            "description": "No idea with that id in the caller's business (error code `not_found`)."
          },
          "422": {
            "description": "Voting has closed for this idea (error code `voting_closed`)."
          }
        }
      },
      "delete": {
        "tags": [
          "Ideas"
        ],
        "summary": "Remove the caller's upvote from an idea",
        "description": "Removes the caller's upvote. The native mirror of the web\nApps::Ideas::VotesController#destroy.\n\n**Gated by the \"Allow removing an upvote\" workspace setting\n(`vote_change`).** When that setting is OFF the upvote is final: this\nendpoint returns `403 vote_change_disabled` and the vote is kept. The\ngate is checked BEFORE any delete, so it applies even when the caller has\nno vote to remove. When the setting is ON, removal is **idempotent** —\nremoving with no vote present is a no-op `200`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Vote removed (or none was present).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdeaVoteState"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Ideas app is not accessible to the caller (error code `access_denied`), OR removing an upvote is disabled for the workspace (error code `vote_change_disabled`)."
          },
          "404": {
            "description": "No idea with that id in the caller's business (error code `not_found`)."
          }
        }
      }
    },
    "/ideas/{idea_id}/score": {
      "parameters": [
        {
          "name": "idea_id",
          "in": "path",
          "required": true,
          "description": "The idea being scored.",
          "schema": {
            "type": "integer",
            "example": 91
          }
        }
      ],
      "patch": {
        "tags": [
          "Ideas"
        ],
        "summary": "Add or update an idea's RICE score (reviewer)",
        "description": "Writes the four RICE inputs — **Reach, Impact, Confidence, Effort** — onto an\nidea, the native mirror of the web reviewer's Stage-Management score form\n(`Apps::Ideas::IdeasController#score`). Both surfaces call the same\n`::Ideas::ScoreUpdateService`, so the coercion, the effort floor and the\naudit trail cannot drift.\n\n**Gates — identical to the web, in this order:**\n1. The Ideas app must be accessible to the caller → `403 access_denied`.\n2. RICE scoring must be enabled for the workspace → `403 scoring_disabled`.\n3. The caller must be on **this idea's review panel** (its campaign's group\n   when it has one, else the workspace default) → `403 not_a_reviewer`.\n   **Panel membership is the only grant** — an Ideas admin who is not on the\n   panel is refused, exactly as on the web.\n4. The idea must exist in the caller's business → `404 not_found` (a foreign\n   id is indistinguishable from one that never existed).\n\n**Field rules**\n* `reach` is stored as an integer; `impact`, `confidence` and `effort` as floats.\n* `effort` is floored at **0.25** so RICE never divides by zero — but only\n  when a value is actually supplied.\n* Sending a field **empty** CLEARS it (stores null); **omitting** a field\n  leaves the stored value untouched (partial update).\n* `confidence` is optional — when null, RICE treats it as `1.0`.\n\n**RICE only computes when Reach, Impact and Effort are all present.** When one\nis blank the values still save but the idea stays unscored: the response\nreturns `scored: false` and `rice_score: null`, and `message` says so rather\nthan implying a score landed.\n\n`rice_score = round(reach × impact × confidence ÷ max(effort, 0.25))`\n\n**Idempotent** — re-sending the same values is a no-op save, and the audit\nlogger coalesces a reviewer's rapid successive edits into a single entry\n(dropping it entirely if they revert to where the session started).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "idea"
                ],
                "properties": {
                  "idea": {
                    "type": "object",
                    "description": "The RICE inputs. All four are optional: omit a field to leave it unchanged, send it empty to clear it.",
                    "properties": {
                      "reach": {
                        "type": "integer",
                        "nullable": true,
                        "description": "How many people this affects (stored as an integer).",
                        "example": 100
                      },
                      "impact": {
                        "type": "number",
                        "nullable": true,
                        "description": "Impact multiplier per person.",
                        "example": 2
                      },
                      "confidence": {
                        "type": "number",
                        "nullable": true,
                        "description": "Confidence factor (0–1). Optional — null is treated as 1.0.",
                        "example": 0.5
                      },
                      "effort": {
                        "type": "number",
                        "nullable": true,
                        "description": "Person-months of effort. Floored at 0.25 when supplied.",
                        "example": 2
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Score saved (see `scored` for whether RICE could be computed)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "idea_id",
                    "rice_score",
                    "rice_score_band",
                    "scored",
                    "score",
                    "message"
                  ],
                  "properties": {
                    "idea_id": {
                      "type": "integer",
                      "example": 91
                    },
                    "rice_score": {
                      "type": "integer",
                      "nullable": true,
                      "description": "The computed RICE score, or null when a required input (reach / impact / effort) is blank.",
                      "example": 50
                    },
                    "rice_score_band": {
                      "type": "object",
                      "description": "The colour band `rice_score` renders as, so a client paints the score pill the same way the web does without hardcoding our ranges. `tier` is the stable key to switch on: `high` (>= 700), `medium` (>= 300), `low` below that, `none` when the values saved but RICE could not be computed. Always present here — this endpoint 403s outright when RICE scoring is off — so a client can recolour the pill straight off this response instead of refetching the row.",
                      "properties": {
                        "tier": {
                          "type": "string",
                          "enum": [
                            "high",
                            "medium",
                            "low",
                            "none"
                          ],
                          "example": "high"
                        },
                        "label": {
                          "type": "string",
                          "description": "Human label for the band.",
                          "example": "High"
                        },
                        "color": {
                          "type": "string",
                          "description": "Foreground hex the web uses.",
                          "example": "#146c43"
                        },
                        "background": {
                          "type": "string",
                          "description": "Background hex the web uses; `transparent` for the `none` band.",
                          "example": "#d1f0e0"
                        }
                      }
                    },
                    "scored": {
                      "type": "boolean",
                      "description": "False when the values saved but the idea remains unscored — it is then invisible to the score sort and the Value/Effort matrix.",
                      "example": true
                    },
                    "score": {
                      "type": "object",
                      "description": "The stored inputs after coercion (nulls for cleared fields).",
                      "required": [
                        "reach",
                        "impact",
                        "confidence",
                        "effort"
                      ],
                      "properties": {
                        "reach": {
                          "type": "integer",
                          "nullable": true,
                          "example": 100
                        },
                        "impact": {
                          "type": "number",
                          "nullable": true,
                          "example": 2
                        },
                        "confidence": {
                          "type": "number",
                          "nullable": true,
                          "example": 0.5
                        },
                        "effort": {
                          "type": "number",
                          "nullable": true,
                          "example": 2
                        }
                      }
                    },
                    "message": {
                      "type": "string",
                      "description": "\"Score updated.\" when RICE computed, otherwise an explanation that Reach, Impact and Effort are all required.",
                      "example": "Score updated."
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The `idea` object was missing from the body (`parameter_missing`)."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Ideas app is not accessible (`access_denied`), RICE scoring is turned off for the workspace (`scoring_disabled`), or the caller is not on this idea's review panel (`not_a_reviewer` — no admin bypass)."
          },
          "404": {
            "description": "No idea with that id in the caller's business (error code `not_found`)."
          },
          "422": {
            "description": "The score could not be saved (`score_failed`)."
          }
        }
      }
    },
    "/ideas/{idea_id}/comments": {
      "parameters": [
        {
          "name": "idea_id",
          "in": "path",
          "required": true,
          "description": "The idea id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Ideas"
        ],
        "summary": "An idea's discussion thread",
        "description": "A paginated list of the idea's **top-level** comments, each inlining its\ndirect replies. Threading is ONE level deep, matching the web idea detail\n(`Apps::Ideas::IdeasController#show`) — a reply always has `replies: []`.\nModelled on the Wikis comment API.\n\n* **Ordering** — oldest first, like the web thread, with an `id` tiebreak\n  so comments sharing a `created_at` never repeat or get skipped across\n  page boundaries.\n* **`meta.total_count` counts TOP-LEVEL comments** — that is what\n  pagination is over. **`total_comments`** is the full discussion volume\n  (comments *plus* replies), which is the number the feed card's\n  `comments_count` shows, so a client's header keeps agreeing with the\n  card it came from.\n* **Inlined `replies` are capped at 20 per comment.** `per_page` bounds the\n  TOP-LEVEL list only, so without a cap one response could carry 50 threads\n  × every reply they have (~1.7 MB). `replies_count` is the TRUE total from\n  a COUNT — it can exceed `replies.length` — and `replies_truncated` says\n  the array is partial. Fetch the rest in *replies mode* (next bullet); its\n  default `per_page` is the same 20, so `page=2` continues exactly where\n  the inlined array stopped, with no overlap and no gap.\n* **`parent_comment_id`** switches to *replies mode*: pass a top-level\n  comment's id to page through that comment's replies instead of the\n  thread. An id that is unknown, names a reply, or belongs to a different\n  idea returns an empty page (not an error) — the parent is resolved\n  through this idea's own comments, so a forged id can never widen the\n  result.\n* Soft-deleted comments never appear, and are excluded from both counts.\n* Page size defaults to 20 (max 50). The web renders a fixed 25 per page;\n  this surface exposes client-controllable paging like every other Ideas\n  endpoint, while using the identical ordering.\n\nReading needs no permission beyond access to the Ideas app; a foreign or\nmissing idea id returns `404`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "parent_comment_id",
            "in": "query",
            "required": false,
            "description": "When present, return the REPLIES of this top-level comment instead of the top-level thread.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based page number (default 1).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Rows per page (default 20, maximum 50).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Thread retrieved successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "idea_id",
                    "comments",
                    "total_comments",
                    "meta"
                  ],
                  "properties": {
                    "idea_id": {
                      "type": "integer",
                      "description": "The idea id (echoes the path id).",
                      "example": 42
                    },
                    "comments": {
                      "type": "array",
                      "description": "Top-level comments (or replies, in replies mode), oldest first.",
                      "items": {
                        "$ref": "#/components/schemas/IdeaComment"
                      }
                    },
                    "total_comments": {
                      "type": "integer",
                      "description": "The idea's full discussion volume — comments plus replies, excluding soft-deleted rows. Matches the feed card's `comments_count`, and is never below `meta.total_count`.",
                      "example": 9
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "total_count",
                        "current_page",
                        "per_page",
                        "total_pages"
                      ],
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "TOP-LEVEL comments (what pagination is over), or replies in replies mode.",
                          "example": 4
                        },
                        "current_page": {
                          "type": "integer",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "example": 20
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "0 when the thread is empty.",
                          "example": 1
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Ideas app is not accessible to the caller (error code `access_denied`)."
          },
          "404": {
            "description": "No idea with that id in the caller's business (error code `not_found`)."
          }
        }
      },
      "post": {
        "tags": [
          "Ideas"
        ],
        "summary": "Post a comment or reply on an idea",
        "description": "Posts a comment on an idea, or a **reply** when `parent_comment_id` is\ngiven. The native mirror of the web\n`Apps::Ideas::CommentsController#create`, sharing\n`Platform::Commentable#post_comment` so both surfaces store a comment —\nand resolve its mentions — identically.\n\n**Mentions** — send the platform-standard `@[Name](mention:id)` tokens\ninline in `body` (the same markup the web and feeds composers emit). The\nserver resolves them to `mentioned_user_ids` and notifies each mentioned\nuser. The raw tokens are preserved in the returned `body` so a client can\nlinkify them.\n\n**No attachments** — an idea comment is text + mentions only, matching the\nweb composer (which posts a bare `comment[body]` with no file field). An\nidea comment is a plain `Platform::Comment`, which declares no attachment\nassociation, so a stray `attachments` param is ignored rather than stored.\n\n**Threading is ONE level.** `parent_comment_id` must name a TOP-LEVEL\ncomment of THIS idea; replying to a reply returns `422`\n`reply_depth_exceeded`, and a parent from another idea's thread returns\n`422` `parent_not_found`.\n\n**Notifications** replicate the web fan-out: the idea's author is told\nabout the activity, a reply ALSO notifies the author of the comment being\nreplied to, and @mentions ping everyone tagged EXCEPT those recipients and\nthe commenter — so one post never sends the same person both a reply and a\nmention. Delivery is best-effort and never fails an accepted comment.\n\n**At most 25 @mentioned people are notified per comment**, and delivery is\nqueued rather than done inside this request (so a 201 means \"accepted\", not\n\"delivered\"). The cap is on the NOTIFICATION only: the comment always saves\nwith its full `mentioned_user_ids`, every mention still renders and still\ncounts for the recipient's Mentions filter. `mentioned_user_ids` is returned\nin full, so a client can compare its length with 25 to know the cap bit. A\ncomment naming more than two dozen people individually is a broadcast — use\nBroadcast or a campaign for that audience.\n\nReturns the created comment in the same shape the thread listing uses,\nplus `total_comments` (comments + replies) so a client can patch the count\nit is already showing without re-fetching.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "properties": {
                  "body": {
                    "type": "string",
                    "description": "Comment text; may contain `@[Name](mention:id)` mention tokens. Blank/whitespace-only is rejected.",
                    "example": "Great idea @[Casey Poster](mention:49290)"
                  },
                  "parent_comment_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Top-level comment id to reply to. Omit (or send blank) for a new top-level comment."
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "properties": {
                  "body": {
                    "type": "string",
                    "example": "Great idea @[Casey Poster](mention:49290)"
                  },
                  "parent_comment_id": {
                    "type": "integer",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Comment created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "comment",
                    "total_comments"
                  ],
                  "properties": {
                    "comment": {
                      "$ref": "#/components/schemas/IdeaComment"
                    },
                    "total_comments": {
                      "type": "integer",
                      "description": "The idea's discussion volume after the post (comments + replies) — the same number the feed card's `comments_count` shows.",
                      "example": 7
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Ideas app is not accessible to the caller (error code `access_denied`)."
          },
          "404": {
            "description": "No idea with that id exists in the caller's business (error code `not_found`)."
          },
          "422": {
            "description": "The body was blank (`body_required`), or `parent_comment_id` was invalid: `parent_not_found` (not a comment on this idea) or `reply_depth_exceeded` (it is itself a reply). Also `content_blocked` when the workspace's content-moderation policy refuses the submitted text; the message is the policy's own and is safe to show the author verbatim."
          }
        }
      }
    },
    "/ideas/{idea_id}/comments/{id}": {
      "parameters": [
        {
          "name": "idea_id",
          "in": "path",
          "required": true,
          "description": "The idea the comment belongs to.",
          "schema": {
            "type": "integer",
            "example": 91
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The comment (or reply) to edit or delete.",
          "schema": {
            "type": "integer",
            "example": 5512
          }
        }
      ],
      "patch": {
        "tags": [
          "Ideas"
        ],
        "summary": "Edit a comment or reply on an idea",
        "description": "Edits ONE comment — or one reply — on an idea: its body and its @mentions.\n\n**AUTHOR ONLY.** Only the comment's own author may edit it, with **no time\nwindow**. This is deliberately *narrower* than the `DELETE` on this same\npath, which also admits an Ideas admin: deleting is a moderation action the\nweb grants admins, whereas editing would let an admin rewrite words that\nstay attributed to their original author. An Ideas admin who is not the\nauthor gets `403 forbidden` and the comment is untouched.\n\nNote the web Ideas thread has **no edit affordance at all**\n(`Apps::Ideas::CommentsController` implements only create + destroy), so\nthis endpoint follows the platform's comment-edit precedent — WikiComment\nand News Feed both restrict editing to the author — minus their expiry\nwindow, matching how Ideas already declines to time-limit its deletes.\n\nThe `can_edit` flag on every comment and reply returned by\n`GET /ideas/{idea_id}/comments` is the SAME decision, so a client can render\nthe Edit affordance exactly where this call will succeed.\n\n**@mentions are re-derived from the new body.** Send the platform mention\ntoken `@[Name](mention:id)` inline, exactly as on create; the server\nre-parses the edited body and rewrites `mentioned_user_ids`, so a mention\nyou add starts counting and one you delete stops. A token naming a user\noutside this business is dropped rather than stored. Only the users this\nedit NEWLY mentions are notified — fixing a typo never re-pings the thread —\nand **at most 25 of them**, queued rather than delivered inside this request\n(the same cap and the same queueing as `POST .../comments`; it bounds the\nnotification only, never what is stored). Omit `body` entirely to leave the\ntext (and therefore its mentions) untouched.\n\n**No attachments** — an idea comment is text + mentions only on every\nsurface (matching the web composer, which has no file field), so there are\nno files to add or remove. A stray `attachments` param is ignored.\n\nA successful edit stamps `edited_at`, which the comment payload returns so a\nclient can show an \"edited\" marker.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "body": {
                    "type": "string",
                    "description": "The new text (leading/trailing whitespace trimmed, like create). Mentions are re-derived from it. OMIT to leave the body and its mentions unchanged; sending it blank is rejected `422 body_required` rather than silently emptying the comment.",
                    "example": "Revised — cc @[Ada Adminson](mention:42)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Comment updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "comment"
                  ],
                  "properties": {
                    "comment": {
                      "$ref": "#/components/schemas/IdeaComment"
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Ideas app is not accessible to the caller (error code `access_denied`), OR the caller is not the comment's author (error code `forbidden`) — an Ideas admin who did not write it is refused here too. The comment is left untouched."
          },
          "404": {
            "description": "No such comment in this idea's thread within the caller's business (error code `not_found`). Covers an unknown id, a comment belonging to a DIFFERENT idea, a comment in another tenant, and an already-deleted comment — all report `404`, never `403`."
          },
          "422": {
            "description": "`body_required` — `body` was sent but blank; `invalid` — the resulting comment failed validation; `edit_failed` — the write could not be completed. In every case the comment is left exactly as it was. Also `content_blocked` when the workspace's content-moderation policy refuses the submitted text; the message is the policy's own and is safe to show the author verbatim."
          }
        }
      },
      "delete": {
        "tags": [
          "Ideas"
        ],
        "summary": "Delete a comment or reply on an idea",
        "description": "Deletes ONE comment — or one reply — from an idea's discussion. The native\nmirror of the web `Apps::Ideas::CommentsController#destroy` (the **Delete**\naffordance on each comment in the thread).\n\n**AUTHOR OR IDEAS ADMIN.** The comment's own author may delete it, and so may\nan Ideas admin — a business admin-or-above, or a member holding the Ideas\napp-admin role. This is the web gate verbatim, and there is **no time window**\n(unlike the Wikis comment API's 5-minute edit/delete window). Anyone else gets\n`403 forbidden` and the comment is left untouched.\n\nNote this gate is *wider* than the sibling `DELETE /ideas/{id}`, which is\nauthor-only: removing a comment is a moderation action the web grants admins,\nwhereas deleting an idea has no admin affordance at all. Authoring the **idea**\ngrants nothing here — the gate reads the comment's author, so the idea's author\ncannot remove other people's comments from their own idea.\n\nThe `can_delete` flag on every comment and reply returned by\n`GET /ideas/{idea_id}/comments` is the SAME decision, so a client can render the\nDelete affordance exactly where this call will succeed.\n\n**A soft delete.** `Platform::Comment` is soft-deletable: the row is retained\nwith `deleted_at` (and `deleted_by_id`) stamped, and every read path filters it\nout — the web thread, this API's thread, and every comment count. It is gone as\nfar as any user is concerned, and it is not restorable through the API.\n\n**Replies go with it.** Deleting a top-level comment also deletes its direct\nreplies — the cascade the web's own confirm dialog promises (\"Its replies will\nbe removed too.\"). `replies_deleted` reports how many went. Replies that were\nalready deleted are not re-stamped and are not counted. Deleting a **reply**\ntouches only that reply: its parent and its siblings are untouched, and\n`replies_deleted` is `0` (threading is one level deep, so a reply has none).\n\n**Not idempotent.** A second call returns `404 not_found`, because the comment\nis genuinely gone from every scope the lookup can see. Clients should treat\n`404` on a retry as success.\n\n`total_comments` is the idea's discussion volume (comments + replies) **after**\nthe delete, in the same key `GET /ideas/{idea_id}/comments` uses, so a client can\npatch the count it is already displaying without re-fetching the thread.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Comment deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "idea_id",
                    "comment_id",
                    "parent_comment_id",
                    "deleted",
                    "replies_deleted",
                    "total_comments",
                    "message"
                  ],
                  "properties": {
                    "idea_id": {
                      "type": "integer",
                      "description": "The idea the comment belonged to.",
                      "example": 91
                    },
                    "comment_id": {
                      "type": "integer",
                      "description": "The id of the comment that was deleted.",
                      "example": 5512
                    },
                    "parent_comment_id": {
                      "type": "integer",
                      "nullable": true,
                      "description": "The deleted comment's parent — `null` when a top-level comment was deleted, the parent's id when a reply was.",
                      "example": null
                    },
                    "deleted": {
                      "type": "boolean",
                      "description": "Always true on a 200.",
                      "example": true
                    },
                    "replies_deleted": {
                      "type": "integer",
                      "description": "Direct replies deleted along with the comment (already-deleted replies are not counted). Always 0 when the deleted record was itself a reply.",
                      "example": 2
                    },
                    "total_comments": {
                      "type": "integer",
                      "description": "The idea's remaining discussion volume — comments + replies, the same number `GET /ideas/{idea_id}/comments` reports and the feed card's `comments_count` shows.",
                      "example": 7
                    },
                    "message": {
                      "type": "string",
                      "description": "`Comment deleted.` for a top-level comment, `Reply deleted.` for a reply.",
                      "example": "Comment deleted."
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Ideas app is not accessible to the caller (error code `access_denied`), OR the caller is neither the comment's author nor an Ideas admin (error code `forbidden`). The comment is left untouched."
          },
          "404": {
            "description": "No such comment in this idea's thread within the caller's business (error code `not_found`). Covers an unknown id, a comment that belongs to a DIFFERENT idea, a comment in another tenant, and a comment that is already deleted — all report `404`, never `403`, so the response can't confirm that the id exists somewhere else."
          },
          "422": {
            "description": "The delete could not be completed (error code `delete_failed`); the comment and its replies survive."
          }
        }
      }
    },
    "/ideas/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The idea's id.",
          "schema": {
            "type": "integer",
            "example": 91
          }
        }
      ],
      "get": {
        "tags": [
          "Ideas"
        ],
        "summary": "Single idea detail",
        "description": "The full detail for ONE idea — the native mirror of the web detail page\n(`Apps::Ideas::IdeasController#show`). All loading runs through the shared\n`::Ideas::IdeaDetailQuery`, so the web and the API resolve the reviewer\npanel, the recent-voter set, the lifecycle and the comment count\nidentically, in a fixed number of queries (no N+1).\n\n**Lifecycle** — every lifecycle stage of the workspace, in pipeline\n(`position`) order, each marked relative to where this idea sits:\n`done` (before the current stage), `current` (this idea's stage), or\n`upcoming` (after it). This is the strip the web renders.\n\n**Reviewer panel** — `group_name` is the panel that scores this idea (the\nidea's campaign group when it has one, else the workspace default — the\nweb's \"This idea is scored by …\"). `reviewers` carries up to **3** members\nand is **OMITTED** entirely when the workspace turns \"Show reviewer names\"\noff; `reviewers_count` (the whole panel size) is always present so a client\ncan still render \"Review Panel (7)\" exactly like the web.\n\n**Conditional fields**\n* `voting_closes_on` — present ONLY when the workspace enabled the\n  voting-close-date feature AND this idea has a date set (key omitted\n  otherwise, never null).\n* `rice_score` / `rice_score_band` — present only while RICE scoring is on\n  for the workspace. The band is the colour the web paints the score.\n* `attachments` — `[]` when the workspace turned attachments off.\n\n**`recent_voters`** — the three most recent upvoters, newest first, each\nwith the time they voted.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Idea detail retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "idea"
                  ],
                  "properties": {
                    "idea": {
                      "type": "object",
                      "required": [
                        "id",
                        "title",
                        "description",
                        "description_html",
                        "stage",
                        "creator",
                        "created_at",
                        "vote_count",
                        "has_voted",
                        "voting_closed",
                        "vote_change_allowed",
                        "can_edit",
                        "can_delete",
                        "group_name",
                        "campaign",
                        "comments_count",
                        "lifecycle",
                        "reviewers_count",
                        "recent_voters",
                        "attachments"
                      ],
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 91
                        },
                        "title": {
                          "type": "string",
                          "example": "Add SSO for contractors"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "description": "The idea's plain-text body.",
                          "example": "We should let contractors sign in with SSO."
                        },
                        "description_html": {
                          "type": "string",
                          "nullable": true,
                          "description": "The rich-text body as stored (safe HTML).",
                          "example": "<p>We should let contractors sign in with SSO.</p>"
                        },
                        "stage": {
                          "type": "object",
                          "description": "The idea's current lifecycle stage.",
                          "required": [
                            "id",
                            "name",
                            "color",
                            "category"
                          ],
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 3
                            },
                            "name": {
                              "type": "string",
                              "example": "Reviewing"
                            },
                            "color": {
                              "type": "string",
                              "description": "The stage's hex colour.",
                              "example": "#2e63b3"
                            },
                            "category": {
                              "type": "string",
                              "enum": [
                                "entry",
                                "active",
                                "implemented",
                                "declined"
                              ],
                              "example": "active"
                            }
                          }
                        },
                        "creator": {
                          "type": "object",
                          "required": [
                            "id",
                            "name",
                            "photo"
                          ],
                          "properties": {
                            "id": {
                              "type": "integer",
                              "nullable": true,
                              "example": 49290
                            },
                            "name": {
                              "type": "string",
                              "nullable": true,
                              "description": "The author's full name.",
                              "example": "Dana Lee"
                            },
                            "photo": {
                              "type": "string",
                              "nullable": true,
                              "description": "Absolute avatar URL (a ui-avatars initial tile when the user has no photo).",
                              "example": "https://officechat.workforce.mangoapps.com/system/photos/49290/thumb.png"
                            }
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-07-31T06:01:58Z"
                        },
                        "vote_count": {
                          "type": "integer",
                          "description": "Positive votes (up_votes_count counter cache).",
                          "example": 12
                        },
                        "has_voted": {
                          "type": "boolean",
                          "description": "Whether the CALLING user has upvoted this idea.",
                          "example": true
                        },
                        "voting_closed": {
                          "type": "boolean",
                          "description": "True once the idea's voting close date has passed — but only on a workspace where `close_date_enabled` is true. A workspace that has the voting-close-date feature off never reports a closed idea and never refuses a vote, and `voting_closes_on` is withheld.",
                          "example": false
                        },
                        "vote_change_allowed": {
                          "type": "boolean",
                          "description": "Whether the workspace lets a member remove their upvote (drives the Remove affordance).",
                          "example": true
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "Whether the CALLING user may edit this idea. True for the **author only** — the web gates edit on `set_own_idea` (`Idea#mine?`) and there is deliberately **no admin override** (its Manage dropdown renders solely under `mine?`). Drives the Edit affordance.",
                          "example": false
                        },
                        "can_delete": {
                          "type": "boolean",
                          "description": "Whether the CALLING user may delete this idea — the same author-only rule as `can_edit`, and exactly what `DELETE /ideas/{id}` enforces. Kept as a separate field because it is a separate affordance.",
                          "example": false
                        },
                        "rice_score": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Cached RICE score (null when the idea has not been scored). The key is OMITTED entirely when RICE scoring is turned off for the workspace.",
                          "example": 42
                        },
                        "rice_score_band": {
                          "type": "object",
                          "description": "The colour band `rice_score` renders as, so a client paints the score pill the same way the web does without hardcoding our ranges. `tier` is the stable key to switch on: `high` (>= 700), `medium` (>= 300), `low` below that, `none` when the idea is awaiting its first score. Travels with `rice_score` and is OMITTED under the same gate (RICE scoring off for the workspace).",
                          "properties": {
                            "tier": {
                              "type": "string",
                              "enum": [
                                "high",
                                "medium",
                                "low",
                                "none"
                              ],
                              "example": "high"
                            },
                            "label": {
                              "type": "string",
                              "description": "Human label for the band.",
                              "example": "High"
                            },
                            "color": {
                              "type": "string",
                              "description": "Foreground hex the web uses.",
                              "example": "#146c43"
                            },
                            "background": {
                              "type": "string",
                              "description": "Background hex the web uses; `transparent` for the `none` band.",
                              "example": "#d1f0e0"
                            }
                          }
                        },
                        "group_name": {
                          "type": "string",
                          "nullable": true,
                          "description": "Name of the reviewer panel that scores this idea — the idea's campaign review group when it has one, else the workspace default panel. null when no panel is configured.",
                          "example": "Product Review Panel"
                        },
                        "campaign": {
                          "type": "object",
                          "nullable": true,
                          "description": "The campaign this idea was submitted to; null when it stands alone.",
                          "required": [
                            "id",
                            "name"
                          ],
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 5
                            },
                            "name": {
                              "type": "string",
                              "example": "Frontline Experience"
                            }
                          }
                        },
                        "comments_count": {
                          "type": "integer",
                          "description": "Non-deleted comments + replies.",
                          "example": 4
                        },
                        "voting_closes_on": {
                          "type": "string",
                          "format": "date",
                          "description": "The idea's voting close date (ISO `YYYY-MM-DD`). Present ONLY when the workspace enabled the voting-close-date feature AND this idea has a date set — the key is OMITTED otherwise.",
                          "example": "2026-08-15"
                        },
                        "lifecycle": {
                          "type": "array",
                          "description": "Every workspace lifecycle stage in pipeline order, marked relative to this idea's current stage.",
                          "items": {
                            "type": "object",
                            "required": [
                              "stage_id",
                              "name",
                              "color",
                              "position",
                              "state"
                            ],
                            "properties": {
                              "stage_id": {
                                "type": "integer",
                                "example": 3
                              },
                              "name": {
                                "type": "string",
                                "example": "Reviewing"
                              },
                              "color": {
                                "type": "string",
                                "example": "#2e63b3"
                              },
                              "position": {
                                "type": "integer",
                                "description": "0-based pipeline position.",
                                "example": 1
                              },
                              "state": {
                                "type": "string",
                                "enum": [
                                  "done",
                                  "current",
                                  "upcoming"
                                ],
                                "description": "`done` before the idea's stage, `current` at it, `upcoming` after it.",
                                "example": "current"
                              }
                            }
                          }
                        },
                        "reviewers": {
                          "type": "array",
                          "description": "Up to THREE members of the idea's reviewer panel. OMITTED entirely when \"Show reviewer names\" is off for the workspace (`reviewers_count` still reports the full panel size).",
                          "items": {
                            "type": "object",
                            "required": [
                              "id",
                              "name",
                              "photo"
                            ],
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 77
                              },
                              "name": {
                                "type": "string",
                                "nullable": true,
                                "example": "Rae Reviewer"
                              },
                              "photo": {
                                "type": "string",
                                "nullable": true,
                                "description": "Absolute avatar URL.",
                                "example": "https://officechat.workforce.mangoapps.com/system/photos/77/thumb.png"
                              }
                            }
                          }
                        },
                        "reviewers_count": {
                          "type": "integer",
                          "description": "Total size of the reviewer panel (present even when names are hidden).",
                          "example": 7
                        },
                        "is_reviewer": {
                          "type": "boolean",
                          "description": "Whether the CALLER is on this idea's review panel — the panel its campaign sets, else the workspace default. This is the web's `can_review?` rule verbatim: **panel membership is the only grant**, so an Ideas admin who is not a member gets `false` (the web's reviewer actions carry no admin bypass either). Drives the reviewer-only affordances a client renders — the stage mover, the RICE scorer — and gates `audit_entries` below.",
                          "example": true
                        },
                        "audit_entries": {
                          "type": "array",
                          "description": "The idea's audit trail, newest first, capped at 50 — **present only when `is_reviewer` is true**. The web renders its Audit Log panel behind the same gate: the trail names who moved a stage, what the RICE score was before, and reviewer notes, none of which a submitter is shown.\nThe key is OMITTED for a non-reviewer rather than sent empty: `[]` would read as \"this idea has no history\", which is a different claim from \"not yours to see\".\nEach row carries the structured facts (not a pre-built sentence), so a client composes its own copy. `payload` is passed through verbatim so nothing the web can show is lost — a stage `note`, the per-field RICE `changes`, the `edits` list.",
                          "items": {
                            "type": "object",
                            "required": [
                              "id",
                              "entry_type",
                              "actor",
                              "created_at",
                              "icon",
                              "color",
                              "payload"
                            ],
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 9013
                              },
                              "entry_type": {
                                "type": "string",
                                "description": "What happened. `create` = the idea was submitted.",
                                "enum": [
                                  "create",
                                  "stage",
                                  "score",
                                  "edit"
                                ],
                                "example": "stage"
                              },
                              "actor": {
                                "type": "object",
                                "nullable": true,
                                "description": "Who did it; null for a system entry.",
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 77
                                  },
                                  "name": {
                                    "type": "string",
                                    "example": "Rae Reviewer"
                                  },
                                  "photo": {
                                    "type": "string",
                                    "nullable": true,
                                    "description": "Absolute avatar URL."
                                  }
                                }
                              },
                              "created_at": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2026-07-31T09:30:28Z"
                              },
                              "icon": {
                                "type": "string",
                                "description": "Font Awesome glyph for the entry type — the same one the web uses, so both surfaces read identically.",
                                "example": "fa-arrow-right-arrow-left"
                              },
                              "color": {
                                "type": "string",
                                "description": "Accent hex for the entry type (matches the web).",
                                "example": "#2e63b3"
                              },
                              "payload": {
                                "type": "object",
                                "description": "The raw, type-specific detail. `stage` → `{from, to, note?}` (stage ids); `score` → `{score_from, score_to, changes: [{field, from, to}]}`; `edit` → `{edits: [...]}`; `create` → `{}`.",
                                "additionalProperties": true,
                                "example": {
                                  "from": 1,
                                  "to": 2,
                                  "note": "Looks good"
                                }
                              },
                              "from_stage": {
                                "type": "object",
                                "nullable": true,
                                "description": "`stage` entries ONLY (absent on other types): the payload's `from` id resolved for rendering. Null when the entry has no `from` (an entry INTO the pipeline) or the stage has since been deleted.",
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 1
                                  },
                                  "name": {
                                    "type": "string",
                                    "example": "New"
                                  },
                                  "color": {
                                    "type": "string",
                                    "example": "#6c757d"
                                  }
                                }
                              },
                              "to_stage": {
                                "type": "object",
                                "nullable": true,
                                "description": "`stage` entries ONLY: the payload's `to` id resolved the same way.",
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 2
                                  },
                                  "name": {
                                    "type": "string",
                                    "example": "Reviewing"
                                  },
                                  "color": {
                                    "type": "string",
                                    "example": "#997404"
                                  }
                                }
                              }
                            }
                          }
                        },
                        "audit_entries_truncated": {
                          "type": "boolean",
                          "description": "Whether `audit_entries` above may be only the NEWEST page of the trail. Present exactly when `audit_entries` is — i.e. only for a reviewer — so a flag never appears without the collection it describes.\nThe other two capped collections on this payload ship their real totals (`reviewers_count`, `vote_count`); the audit trail's true total is not available without an extra query, so it ships this flag instead. It is the same detector the web uses for its \"Showing the 50 most recent entries.\" line, which means it is *possibly* truncated: an idea with exactly 50 entries reports `true`. It over-reports by at most one page and never under-reports, so a client may safely use it to decide whether to caveat the list.",
                          "example": false
                        },
                        "recent_voters": {
                          "type": "array",
                          "description": "The three most recent upvoters, newest first.",
                          "items": {
                            "type": "object",
                            "required": [
                              "id",
                              "name",
                              "photo",
                              "voted_at"
                            ],
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 49290
                              },
                              "name": {
                                "type": "string",
                                "nullable": true,
                                "example": "Dana Lee"
                              },
                              "photo": {
                                "type": "string",
                                "nullable": true,
                                "description": "Absolute avatar URL."
                              },
                              "voted_at": {
                                "type": "string",
                                "format": "date-time",
                                "description": "When this person upvoted.",
                                "example": "2026-07-31T06:15:30Z"
                              }
                            }
                          }
                        },
                        "attachments": {
                          "type": "array",
                          "description": "Files attached to the idea. `[]` when the workspace turned attachments off.",
                          "items": {
                            "type": "object",
                            "required": [
                              "id",
                              "filename",
                              "content_type",
                              "byte_size",
                              "url"
                            ],
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 2637
                              },
                              "filename": {
                                "type": "string",
                                "example": "proposal.pdf"
                              },
                              "content_type": {
                                "type": "string",
                                "nullable": true,
                                "example": "application/pdf"
                              },
                              "byte_size": {
                                "type": "integer",
                                "example": 88877
                              },
                              "url": {
                                "type": "string",
                                "description": "Absolute download URL (Content-Disposition attachment).",
                                "example": "https://officechat.workforce.mangoapps.com/rails/active_storage/blobs/redirect/eyJ.../proposal.pdf?disposition=attachment"
                              }
                            }
                          }
                        }
                      }
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Ideas app is not accessible to the caller (error code `access_denied`)."
          },
          "404": {
            "description": "No idea with that id in the caller's business (error code `not_found`)."
          }
        }
      },
      "patch": {
        "tags": [
          "Ideas"
        ],
        "summary": "Edit an idea",
        "description": "Edits an idea — the native mirror of the web Manage ▸ Edit drawer\n(`Apps::Ideas::IdeasController#update`), sharing\n`Ideas::IdeaUpdateService`, `Ideas::AttachmentScreener` and\n`Ideas::DescriptionHtml` so the two surfaces behave identically.\n\n**PATCH semantics** — every field is optional and a key you do NOT send is left\nunchanged, so editing the title can never blank the description. The web always\nposts its whole form, so sending everything behaves the same.\n\n**The campaign is never silently detached.** `campaign_id` is applied only when\nyou send it; omitting it keeps the current campaign. The web learned this the\nhard way — its campaign select renders only when an open campaign exists, so an\nunconditional assign detached ideas whose campaign had since closed, via a\nfield the author could not even see.\n\n**Attachments are ADDITIVE and gated.** `files[]` / `file_signed_ids[]` are\naccepted only while **\"Allow file attachments\"** is ON; when OFF they are\ndropped and the edit still applies. An edit never replaces or removes an\nexisting file — the web has no attachment-removal path at all. The attach runs\n**inside the same transaction as the save**, so a failed attach rolls the edit\nback rather than committing a half-applied change. Anything dropped is reported\nper-file in `attachment_errors` and repeated in `warnings`.\n\n**`warnings` names every field this workspace's settings discarded**, on a 200\nthat applied only part of the edit: a dropped file, a `voting_closes_on` the\nclose-date toggle refused (whether you were setting one or clearing one), a\n`campaign_id` the campaigns toggle refused. Omitted entirely when nothing was\ndropped.\n\n**Authorization: the AUTHOR only** (`Idea#mine?`), with deliberately no admin\noverride — the web's Manage dropdown renders solely under `mine?`, so an admin\ngets `403` too. An id in another tenant returns `404`.\n\nThe edit is **audited with a diff** (`Ideas::AuditLogger.log_edit!`) and the\nrecorded `edits` are echoed back: a title's from/to, that the description\nchanged (without dumping the body), a moved close date, and a campaign move by\nname.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "description": "`application/json`, or `multipart/form-data` when sending `files[]`.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Send only the fields you want to change.",
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Replaces the title. Whitespace is stripped; blank is rejected (422).",
                    "example": "Dark mode for the mobile app (revised)"
                  },
                  "description": {
                    "type": "string",
                    "description": "Replaces the body; `description_html` is re-derived (escaped, then formatted)."
                  },
                  "campaign_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "MOVES the idea to this campaign. Honoured only while Campaigns is on. Omitting it never detaches the current campaign. Must belong to this business and be open, else `422`."
                  },
                  "voting_closes_on": {
                    "type": "string",
                    "format": "date",
                    "nullable": true,
                    "description": "Sets the voting close date; honoured only while that feature is on. Send it EMPTY (`\"\"` or `null`) to CLEAR the date — that is how the web clears it."
                  }
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 200
                  },
                  "description": {
                    "type": "string"
                  },
                  "campaign_id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "voting_closes_on": {
                    "type": "string",
                    "format": "date",
                    "nullable": true
                  },
                  "files[]": {
                    "type": "array",
                    "description": "Files to ADD. Accepted only while \"Allow file attachments\" is ON; otherwise dropped (the edit still applies). Screened against a size cap and a sniffed content-type allowlist.",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    }
                  },
                  "file_signed_ids[]": {
                    "type": "array",
                    "description": "ActiveStorage signed ids, for clients that direct-upload first. Re-screened server-side; an already-attached blob is rejected.",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Idea updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "idea",
                    "edits",
                    "attachment_errors"
                  ],
                  "properties": {
                    "idea": {
                      "type": "object",
                      "description": "The updated idea, in the SAME shape `GET /api/v1/ideas/{id}` returns — one query object and one serializer back both."
                    },
                    "edits": {
                      "type": "array",
                      "description": "The audit diff that was recorded. Empty when nothing actually changed. `Description` carries no from/to — the trail is a summary, not a copy of the body.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "field": {
                            "type": "string",
                            "example": "Title"
                          },
                          "from": {
                            "type": "string",
                            "nullable": true,
                            "example": "Dark mode"
                          },
                          "to": {
                            "type": "string",
                            "nullable": true,
                            "example": "Dark mode (revised)"
                          }
                        }
                      },
                      "example": [
                        {
                          "field": "Title",
                          "from": "Dark mode",
                          "to": "Dark mode (revised)"
                        },
                        {
                          "field": "Description"
                        }
                      ]
                    },
                    "attachment_errors": {
                      "type": "array",
                      "description": "One reason per file that was NOT attached — wrong type, over the size cap, or the workspace's \"Allow file attachments\" setting being off. Always present; empty when everything attached. Never fatal.",
                      "items": {
                        "type": "string"
                      }
                    },
                    "warnings": {
                      "type": "array",
                      "description": "One sentence per field this workspace's settings discarded (a dropped file, a refused `voting_closes_on`, a refused `campaign_id`). OMITTED when nothing was dropped, so the key's presence is the signal that the 200 is a partial write.",
                      "items": {
                        "type": "string"
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller is not the idea's author (error code `forbidden` — the message says \"edit\"), or the Ideas app is not accessible (`access_denied`)."
          },
          "404": {
            "description": "No idea with that id in this business (error code `not_found`)."
          },
          "422": {
            "description": "Validation failed (error code `invalid`) — a blank title, a title over 200 characters, or a campaign that is closed or belongs to another business. Also `content_blocked` when the workspace's content-moderation policy refuses the submitted text; the message is the policy's own and is safe to show the author verbatim."
          }
        }
      },
      "delete": {
        "tags": [
          "Ideas"
        ],
        "summary": "Delete an idea",
        "description": "Permanently deletes an idea — the native mirror of the web\n`Apps::Ideas::IdeasController#destroy` (the Manage ▸ **Delete idea** action\non the idea detail page).\n\n**AUTHOR-ONLY.** Only the user who submitted the idea may delete it. This is\nthe web rule verbatim (`Idea#mine?`): the web renders its Manage dropdown\nsolely for the author, so **there is no admin override** — a workspace admin\nwho did not write the idea gets `403 forbidden`, same as any other member.\nNote this is stricter than reading: `GET /ideas/{id}` is open to every member\nof the business.\n\n**What the delete removes** (all inside ONE transaction — a failure rolls\nthe whole thing back and the idea survives intact):\n* the idea row itself, and its ActiveStorage attachments (blobs purged),\n* its votes — hard-deleted,\n* its audit-trail entries — hard-deleted,\n* its comments and threaded replies — **soft-deleted** (`deleted_at`\n  stamped). The rows are retained, exactly as the web's cascade leaves them,\n  because `Platform::Comment` is soft-deletable. They disappear from every\n  read either way.\n\nThe idea's **campaign is not affected** — only the idea leaves it.\n\n**Not idempotent.** A second call returns `404 not_found`, because the idea\nis genuinely gone. Clients should treat `404` on a retry as success.\n\n`votes_deleted` and `comments_deleted` report what the cascade actually\ntouched, so a client can confirm the destructive scope it warned the user\nabout (\"its votes and comments will be permanently removed\").\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Idea deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "idea_id",
                    "deleted",
                    "votes_deleted",
                    "comments_deleted",
                    "message"
                  ],
                  "properties": {
                    "idea_id": {
                      "type": "integer",
                      "description": "The id of the idea that was deleted.",
                      "example": 91
                    },
                    "deleted": {
                      "type": "boolean",
                      "description": "Always true on a 200.",
                      "example": true
                    },
                    "votes_deleted": {
                      "type": "integer",
                      "description": "Votes removed with the idea.",
                      "example": 12
                    },
                    "comments_deleted": {
                      "type": "integer",
                      "description": "Comments and replies trashed with the idea.",
                      "example": 4
                    },
                    "message": {
                      "type": "string",
                      "example": "Idea deleted."
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Ideas app is not accessible to the caller (error code `access_denied`), OR the caller is not the idea's author (error code `forbidden`). The idea is left untouched."
          },
          "404": {
            "description": "No idea with that id in the caller's business (error code `not_found`). An idea belonging to another tenant reports `404`, never `403`, so the response can't confirm that the id exists elsewhere."
          },
          "422": {
            "description": "The delete could not be completed and was rolled back (error code `delete_failed`); the idea and everything attached to it survive."
          }
        }
      }
    },
    "/training/my_training": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "My Training home",
        "description": "The \"My Training\" home header — the native mirror of the top of the\nlearner dashboard. Returns the three pieces of chrome above the course\nlist (the list itself is the paginated `/training/my_training/enrollments`\nendpoint): the filter-tab counts, the overdue banner, and the \"Continue\nwhere you left off\" cards. Strictly scoped to the caller.\n\n**Sections:**\n* `counts` — badges for the All / In Progress / Assigned / Completed tabs\n  plus the Overdue banner. All but `all` OVERLAP and match the web\n  dashboard chip math: in-progress/assigned/overdue are measured over the\n  active set (an assigned, in-progress course counts in both In Progress\n  and Assigned), completed over the completed set. `all` is their union —\n  every enrollment except cancelled.\n* `overdue` — `count` plus the most-overdue course/path rows, so the\n  client can render the banner (\"<title> is <n> days overdue\").\n* `continue_learning` — up to 4 recently-accessed active COURSE\n  enrollments (newest access first), each a card that additionally\n  carries `next_lesson` (the lesson to resume into — the prototype's\n  \"Next: …\" line).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "My Training home retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "my_training"
                  ],
                  "properties": {
                    "my_training": {
                      "type": "object",
                      "required": [
                        "counts",
                        "overdue",
                        "continue_learning"
                      ],
                      "properties": {
                        "counts": {
                          "type": "object",
                          "description": "Badges for the All / In Progress / Assigned / Completed tabs + Overdue banner. All but `all` OVERLAP (an assigned, in-progress course counts in both In Progress and Assigned). `all` is the union of the others — every enrollment except cancelled — and equals `meta.total_count` on `/training/my_training/enrollments?status=all`.",
                          "required": [
                            "all",
                            "in_progress",
                            "assigned",
                            "completed",
                            "overdue"
                          ],
                          "properties": {
                            "all": {
                              "type": "integer",
                              "example": 8
                            },
                            "in_progress": {
                              "type": "integer",
                              "example": 6
                            },
                            "assigned": {
                              "type": "integer",
                              "example": 3
                            },
                            "completed": {
                              "type": "integer",
                              "example": 2
                            },
                            "overdue": {
                              "type": "integer",
                              "example": 2
                            }
                          }
                        },
                        "overdue": {
                          "type": "object",
                          "required": [
                            "count",
                            "items"
                          ],
                          "properties": {
                            "count": {
                              "type": "integer",
                              "example": 2
                            },
                            "items": {
                              "type": "array",
                              "description": "Most-overdue first; drives the banner text.",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 4412
                                  },
                                  "type": {
                                    "type": "string",
                                    "enum": [
                                      "course",
                                      "path"
                                    ],
                                    "example": "course"
                                  },
                                  "title": {
                                    "type": "string",
                                    "example": "Product Knowledge"
                                  },
                                  "days_overdue": {
                                    "type": "integer",
                                    "example": 15
                                  }
                                }
                              }
                            }
                          }
                        },
                        "continue_learning": {
                          "type": "array",
                          "description": "Up to 4 recently-accessed active COURSE cards, newest access first. Course-only by construction, so the card's path-only fields (completed_steps / total_steps / total_courses / current_step) never appear here; `next_lesson` appears ONLY here (the list endpoint omits it).",
                          "items": {
                            "type": "object",
                            "description": "One course or path enrollment card. `type` selects which progress fields are present.",
                            "required": [
                              "id",
                              "type",
                              "status",
                              "in_progress",
                              "assigned",
                              "completed",
                              "overdue",
                              "progress_percentage",
                              "subject"
                            ],
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 4412
                              },
                              "type": {
                                "type": "string",
                                "enum": [
                                  "course",
                                  "path"
                                ],
                                "example": "course"
                              },
                              "status": {
                                "type": "string",
                                "description": "Raw enrollment status column.",
                                "enum": [
                                  "enrolled",
                                  "in_progress",
                                  "completed",
                                  "cancelled"
                                ],
                                "example": "in_progress"
                              },
                              "status_key": {
                                "type": "string",
                                "description": "Learner-facing state (Training::Display) — note a raw `enrolled` reads as not_started, exactly as the web badge does.",
                                "enum": [
                                  "not_started",
                                  "in_progress",
                                  "completed",
                                  "registered"
                                ],
                                "example": "in_progress"
                              },
                              "status_label": {
                                "type": "string",
                                "description": "Display label for status_key (In Progress / Not Started / Completed / Registered).",
                                "example": "In Progress"
                              },
                              "in_progress": {
                                "type": "boolean",
                                "example": true
                              },
                              "assigned": {
                                "type": "boolean",
                                "description": "Assigned by an admin/manager/rule (renders the ASSIGNED chip).",
                                "example": true
                              },
                              "completed": {
                                "type": "boolean",
                                "example": false
                              },
                              "overdue": {
                                "type": "boolean",
                                "example": false
                              },
                              "progress_percentage": {
                                "type": "integer",
                                "description": "0–100.",
                                "example": 50
                              },
                              "due_date": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true,
                                "example": "2026-08-15T00:00:00Z"
                              },
                              "days_overdue": {
                                "type": "integer",
                                "nullable": true,
                                "description": "Present only when overdue.",
                                "example": null
                              },
                              "completed_at": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true,
                                "example": null
                              },
                              "last_accessed_at": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true,
                                "description": "Courses only.",
                                "example": "2026-08-12T14:03:00Z"
                              },
                              "completed_lessons_count": {
                                "type": "integer",
                                "description": "Course cards only. Counted WITHIN `total_lessons` — completions for lessons outside the learner's pinned version are excluded, so this can never exceed the total.",
                                "example": 3
                              },
                              "total_lessons": {
                                "type": "integer",
                                "description": "Course cards only. The lesson count of the version the learner is PINNED to (falling back to the current published version when not yet pinned) — not the course's lifetime lesson count across every version. Matches the row count of `/training/courses/{id}/lessons` for the same learner.",
                                "example": 6
                              },
                              "next_lesson": {
                                "type": "object",
                                "nullable": true,
                                "description": "Course cards only, and only in Continue-Learning — the lesson to resume into.",
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 91
                                  },
                                  "title": {
                                    "type": "string",
                                    "example": "Lockout/Tagout Procedure"
                                  }
                                }
                              },
                              "completed_steps": {
                                "type": "integer",
                                "description": "Path cards only.",
                                "example": 1
                              },
                              "total_steps": {
                                "type": "integer",
                                "description": "Path cards only.",
                                "example": 3
                              },
                              "total_courses": {
                                "type": "integer",
                                "description": "Path cards only — courses in the path.",
                                "example": 8
                              },
                              "current_step": {
                                "type": "integer",
                                "description": "Path cards only — 1-based \"Step N of M\" (caps at total_steps).",
                                "example": 2
                              },
                              "subject": {
                                "type": "object",
                                "nullable": true,
                                "description": "The course or path. Null for a hidden SYSTEM course (\"course unavailable\").",
                                "required": [
                                  "id",
                                  "title",
                                  "type",
                                  "delivery_label"
                                ],
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 320
                                  },
                                  "title": {
                                    "type": "string",
                                    "example": "Workplace Safety Fundamentals"
                                  },
                                  "slug": {
                                    "type": "string",
                                    "nullable": true,
                                    "example": "workplace-safety-fundamentals"
                                  },
                                  "type": {
                                    "type": "string",
                                    "enum": [
                                      "course",
                                      "path"
                                    ],
                                    "example": "course"
                                  },
                                  "delivery_label": {
                                    "type": "string",
                                    "description": "Self-paced / Instructor-Led / Learning Path.",
                                    "enum": [
                                      "Self-paced",
                                      "Instructor-Led",
                                      "Learning Path"
                                    ],
                                    "example": "Self-paced"
                                  },
                                  "delivery_icon": {
                                    "type": "string",
                                    "description": "Font Awesome glyph for the delivery type — the web banner's type fallback (fa-circle-play / fa-chalkboard-user / fa-route).",
                                    "example": "fa-circle-play"
                                  },
                                  "category": {
                                    "type": "object",
                                    "nullable": true,
                                    "properties": {
                                      "name": {
                                        "type": "string",
                                        "example": "Safety"
                                      },
                                      "color": {
                                        "type": "string",
                                        "nullable": true,
                                        "description": "Category color (hex).",
                                        "example": "#28a745"
                                      },
                                      "icon": {
                                        "type": "string",
                                        "nullable": true,
                                        "description": "Font Awesome glyph.",
                                        "example": "fa-shield-alt"
                                      }
                                    }
                                  },
                                  "hero_image_url": {
                                    "type": "string",
                                    "nullable": true,
                                    "description": "Absolute URL of a resized hero image, or null.",
                                    "example": "https://acme.workforce.mangoapps.com/rails/active_storage/..."
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (error code `access_denied`)."
          }
        }
      }
    },
    "/training/my_training/enrollments": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "My Training course + path list",
        "description": "The \"My Training\" list — course AND training-path enrollments merged into\none feed, newest enrollment first, matching the prototype's mixed list.\nServes the segmented filter: All / In Progress / Assigned / Completed /\nOverdue. Every `status` value takes the same `page`/`per_page` contract\nand returns the same `enrollments` + `counts` + `meta` shape, so a client\ncan back any set of tabs with one request shape.\n\n`counts` badges the tabs + overdue banner (same math as\n`/training/my_training`). `meta` drives pagination. Each row is a\n`TrainingCard` — a `type: course` card carries `completed_lessons_count`/\n`total_lessons`; a `type: path` card carries `completed_steps`/\n`total_steps`/`total_courses`/`current_step` (the prototype's\n\"Step 2 of 3 · 8 courses\").\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "Which tab to load. Unknown values resolve to `in_progress`. `all` is the learner's WHOLE list — the union of every other value (every enrollment except cancelled, which is withdrawn audit state) — so it can never come back smaller than one of them. `overdue` is past-due and not completed. Every value takes the same `page`/`per_page` contract and returns the same `enrollments` + `counts` + `meta` shape.",
            "schema": {
              "type": "string",
              "enum": [
                "in_progress",
                "assigned",
                "completed",
                "overdue",
                "all"
              ],
              "default": "in_progress",
              "example": "in_progress"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Enrollment list retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "enrollments",
                    "active_status",
                    "counts",
                    "meta"
                  ],
                  "properties": {
                    "enrollments": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "One course or path enrollment card. `type` selects which progress fields are present.",
                        "required": [
                          "id",
                          "type",
                          "status",
                          "in_progress",
                          "assigned",
                          "completed",
                          "overdue",
                          "progress_percentage",
                          "subject"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 4412
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "course",
                              "path"
                            ],
                            "example": "course"
                          },
                          "status": {
                            "type": "string",
                            "description": "Raw enrollment status column.",
                            "enum": [
                              "enrolled",
                              "in_progress",
                              "completed",
                              "cancelled"
                            ],
                            "example": "in_progress"
                          },
                          "status_key": {
                            "type": "string",
                            "description": "Learner-facing state (Training::Display) — note a raw `enrolled` reads as not_started, exactly as the web badge does.",
                            "enum": [
                              "not_started",
                              "in_progress",
                              "completed",
                              "registered"
                            ],
                            "example": "in_progress"
                          },
                          "status_label": {
                            "type": "string",
                            "description": "Display label for status_key (In Progress / Not Started / Completed / Registered).",
                            "example": "In Progress"
                          },
                          "in_progress": {
                            "type": "boolean",
                            "example": true
                          },
                          "assigned": {
                            "type": "boolean",
                            "description": "Assigned by an admin/manager/rule (renders the ASSIGNED chip).",
                            "example": true
                          },
                          "completed": {
                            "type": "boolean",
                            "example": false
                          },
                          "overdue": {
                            "type": "boolean",
                            "example": false
                          },
                          "progress_percentage": {
                            "type": "integer",
                            "description": "0–100.",
                            "example": 50
                          },
                          "due_date": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "example": "2026-08-15T00:00:00Z"
                          },
                          "days_overdue": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Present only when overdue.",
                            "example": null
                          },
                          "completed_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "example": null
                          },
                          "last_accessed_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "description": "Courses only.",
                            "example": "2026-08-12T14:03:00Z"
                          },
                          "completed_lessons_count": {
                            "type": "integer",
                            "description": "Course cards only. Counted WITHIN `total_lessons` — completions for lessons outside the learner's pinned version are excluded, so this can never exceed the total.",
                            "example": 3
                          },
                          "total_lessons": {
                            "type": "integer",
                            "description": "Course cards only. The lesson count of the version the learner is PINNED to (falling back to the current published version when not yet pinned) — not the course's lifetime lesson count across every version. Matches the row count of `/training/courses/{id}/lessons` for the same learner.",
                            "example": 6
                          },
                          "next_lesson": {
                            "type": "object",
                            "nullable": true,
                            "description": "Course cards only, and only in Continue-Learning — the lesson to resume into.",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 91
                              },
                              "title": {
                                "type": "string",
                                "example": "Lockout/Tagout Procedure"
                              }
                            }
                          },
                          "completed_steps": {
                            "type": "integer",
                            "description": "Path cards only.",
                            "example": 1
                          },
                          "total_steps": {
                            "type": "integer",
                            "description": "Path cards only.",
                            "example": 3
                          },
                          "total_courses": {
                            "type": "integer",
                            "description": "Path cards only — courses in the path.",
                            "example": 8
                          },
                          "current_step": {
                            "type": "integer",
                            "description": "Path cards only — 1-based \"Step N of M\" (caps at total_steps).",
                            "example": 2
                          },
                          "subject": {
                            "type": "object",
                            "nullable": true,
                            "description": "The course or path. Null for a hidden SYSTEM course (\"course unavailable\").",
                            "required": [
                              "id",
                              "title",
                              "type",
                              "delivery_label"
                            ],
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 320
                              },
                              "title": {
                                "type": "string",
                                "example": "Workplace Safety Fundamentals"
                              },
                              "slug": {
                                "type": "string",
                                "nullable": true,
                                "example": "workplace-safety-fundamentals"
                              },
                              "type": {
                                "type": "string",
                                "enum": [
                                  "course",
                                  "path"
                                ],
                                "example": "course"
                              },
                              "delivery_label": {
                                "type": "string",
                                "description": "Self-paced / Instructor-Led / Learning Path.",
                                "enum": [
                                  "Self-paced",
                                  "Instructor-Led",
                                  "Learning Path"
                                ],
                                "example": "Self-paced"
                              },
                              "delivery_icon": {
                                "type": "string",
                                "description": "Font Awesome glyph for the delivery type — the web banner's type fallback (fa-circle-play / fa-chalkboard-user / fa-route).",
                                "example": "fa-circle-play"
                              },
                              "category": {
                                "type": "object",
                                "nullable": true,
                                "properties": {
                                  "name": {
                                    "type": "string",
                                    "example": "Safety"
                                  },
                                  "color": {
                                    "type": "string",
                                    "nullable": true,
                                    "description": "Category color (hex).",
                                    "example": "#28a745"
                                  },
                                  "icon": {
                                    "type": "string",
                                    "nullable": true,
                                    "description": "Font Awesome glyph.",
                                    "example": "fa-shield-alt"
                                  }
                                }
                              },
                              "hero_image_url": {
                                "type": "string",
                                "nullable": true,
                                "description": "Absolute URL of a resized hero image, or null.",
                                "example": "https://acme.workforce.mangoapps.com/rails/active_storage/..."
                              }
                            }
                          }
                        }
                      }
                    },
                    "active_status": {
                      "type": "string",
                      "enum": [
                        "in_progress",
                        "assigned",
                        "completed",
                        "overdue",
                        "all"
                      ],
                      "example": "in_progress"
                    },
                    "counts": {
                      "type": "object",
                      "description": "Badges for the All / In Progress / Assigned / Completed tabs + Overdue banner. All but `all` OVERLAP (an assigned, in-progress course counts in both In Progress and Assigned). `all` is the union of the others — every enrollment except cancelled — and equals `meta.total_count` on `/training/my_training/enrollments?status=all`.",
                      "required": [
                        "all",
                        "in_progress",
                        "assigned",
                        "completed",
                        "overdue"
                      ],
                      "properties": {
                        "all": {
                          "type": "integer",
                          "example": 8
                        },
                        "in_progress": {
                          "type": "integer",
                          "example": 6
                        },
                        "assigned": {
                          "type": "integer",
                          "example": 3
                        },
                        "completed": {
                          "type": "integer",
                          "example": 2
                        },
                        "overdue": {
                          "type": "integer",
                          "example": 2
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination over the active filter (build_pagination_meta).",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "example": 6
                        },
                        "total_pages": {
                          "type": "integer",
                          "example": 1
                        },
                        "current_page": {
                          "type": "integer",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "example": 20
                        },
                        "has_next_page": {
                          "type": "boolean",
                          "example": false
                        },
                        "has_prev_page": {
                          "type": "boolean",
                          "example": false
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (error code `access_denied`)."
          }
        }
      }
    },
    "/training/courses/{course_id}/enroll": {
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Self-enroll in a course",
        "description": "The action behind the catalog's own `enroll` and `start_path` CTAs\n(`cta_action` on a catalog card), which named an operation that had no\nnative endpoint until now.\n\nONE operation, two routes — `/training/courses/{course_id}/enroll` and\n`/training/learning_paths/{learning_path_id}/enroll` — resolved from\nwhichever id the route carried, the same shape the reviews and Q&A\nwrites use. No request body.\n\n**IDEMPOTENT.** A repeat (double-tapped button, retried request on a\nflaky connection) returns `200` with `created: false` and\n`already_enrolled: true`, NOT an error — so a client may retry safely.\n`created` is the flag to branch on; the accompanying `message` is\nalready worded for either case.\n\n**Both self-enrollment switches must be on**, exactly as every\nlearner-facing surface reads them: the tenant-wide Training setting AND\nthe per-subject admin toggle. Note the two subject defaults differ — a\ncourse allows self-enrollment unless an admin turns it off, a learning\npath denies it unless an admin turns it on — so the same tenant can\nlegitimately answer 200 for a course and 403 for a path.\n\n**Courses additionally enforce prerequisites**; learning paths have no\nprerequisites concept, so that refusal cannot occur for a path. The 422\ncarries the blocking courses in `error.details.missing_prerequisites`\nso the client can name them rather than render a bare refusal.\n\nEnrolling in a PATH creates only the path enrollment — its member\ncourses enroll lazily when the learner opens them, so do not expect\ncourse rows to appear in `/training/my_training/enrollments` yet.\n\nThe returned `enrollment` is the same `TrainingCard` the My Training\nlist renders, so a client can insert it into that list without a\nrefetch.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "description": "The course to enroll in. Digit-constrained.",
            "schema": {
              "type": "integer",
              "example": 128
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Enrolled, or already enrolled — branch on `created`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "created",
                    "already_enrolled",
                    "message",
                    "enrollment"
                  ],
                  "properties": {
                    "created": {
                      "type": "boolean",
                      "description": "True when this request created the enrollment; false on an idempotent repeat.",
                      "example": true
                    },
                    "already_enrolled": {
                      "type": "boolean",
                      "description": "The inverse of `created`, for a client that reads the state rather than the event.",
                      "example": false
                    },
                    "message": {
                      "type": "string",
                      "description": "Learner-facing copy, already worded for the created / already-enrolled case.",
                      "example": "Successfully enrolled!"
                    },
                    "enrollment": {
                      "type": "object",
                      "description": "One course or path enrollment card. `type` selects which progress fields are present.",
                      "required": [
                        "id",
                        "type",
                        "status",
                        "in_progress",
                        "assigned",
                        "completed",
                        "overdue",
                        "progress_percentage",
                        "subject"
                      ],
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 4412
                        },
                        "type": {
                          "type": "string",
                          "enum": [
                            "course",
                            "path"
                          ],
                          "example": "course"
                        },
                        "status": {
                          "type": "string",
                          "description": "Raw enrollment status column.",
                          "enum": [
                            "enrolled",
                            "in_progress",
                            "completed",
                            "cancelled"
                          ],
                          "example": "in_progress"
                        },
                        "status_key": {
                          "type": "string",
                          "description": "Learner-facing state (Training::Display) — note a raw `enrolled` reads as not_started, exactly as the web badge does.",
                          "enum": [
                            "not_started",
                            "in_progress",
                            "completed",
                            "registered"
                          ],
                          "example": "in_progress"
                        },
                        "status_label": {
                          "type": "string",
                          "description": "Display label for status_key (In Progress / Not Started / Completed / Registered).",
                          "example": "In Progress"
                        },
                        "in_progress": {
                          "type": "boolean",
                          "example": true
                        },
                        "assigned": {
                          "type": "boolean",
                          "description": "Assigned by an admin/manager/rule (renders the ASSIGNED chip).",
                          "example": true
                        },
                        "completed": {
                          "type": "boolean",
                          "example": false
                        },
                        "overdue": {
                          "type": "boolean",
                          "example": false
                        },
                        "progress_percentage": {
                          "type": "integer",
                          "description": "0–100.",
                          "example": 50
                        },
                        "due_date": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "example": "2026-08-15T00:00:00Z"
                        },
                        "days_overdue": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Present only when overdue.",
                          "example": null
                        },
                        "completed_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "example": null
                        },
                        "last_accessed_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Courses only.",
                          "example": "2026-08-12T14:03:00Z"
                        },
                        "completed_lessons_count": {
                          "type": "integer",
                          "description": "Course cards only. Counted WITHIN `total_lessons` — completions for lessons outside the learner's pinned version are excluded, so this can never exceed the total.",
                          "example": 3
                        },
                        "total_lessons": {
                          "type": "integer",
                          "description": "Course cards only. The lesson count of the version the learner is PINNED to (falling back to the current published version when not yet pinned) — not the course's lifetime lesson count across every version. Matches the row count of `/training/courses/{id}/lessons` for the same learner.",
                          "example": 6
                        },
                        "next_lesson": {
                          "type": "object",
                          "nullable": true,
                          "description": "Course cards only, and only in Continue-Learning — the lesson to resume into.",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 91
                            },
                            "title": {
                              "type": "string",
                              "example": "Lockout/Tagout Procedure"
                            }
                          }
                        },
                        "completed_steps": {
                          "type": "integer",
                          "description": "Path cards only.",
                          "example": 1
                        },
                        "total_steps": {
                          "type": "integer",
                          "description": "Path cards only.",
                          "example": 3
                        },
                        "total_courses": {
                          "type": "integer",
                          "description": "Path cards only — courses in the path.",
                          "example": 8
                        },
                        "current_step": {
                          "type": "integer",
                          "description": "Path cards only — 1-based \"Step N of M\" (caps at total_steps).",
                          "example": 2
                        },
                        "subject": {
                          "type": "object",
                          "nullable": true,
                          "description": "The course or path. Null for a hidden SYSTEM course (\"course unavailable\").",
                          "required": [
                            "id",
                            "title",
                            "type",
                            "delivery_label"
                          ],
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 320
                            },
                            "title": {
                              "type": "string",
                              "example": "Workplace Safety Fundamentals"
                            },
                            "slug": {
                              "type": "string",
                              "nullable": true,
                              "example": "workplace-safety-fundamentals"
                            },
                            "type": {
                              "type": "string",
                              "enum": [
                                "course",
                                "path"
                              ],
                              "example": "course"
                            },
                            "delivery_label": {
                              "type": "string",
                              "description": "Self-paced / Instructor-Led / Learning Path.",
                              "enum": [
                                "Self-paced",
                                "Instructor-Led",
                                "Learning Path"
                              ],
                              "example": "Self-paced"
                            },
                            "delivery_icon": {
                              "type": "string",
                              "description": "Font Awesome glyph for the delivery type — the web banner's type fallback (fa-circle-play / fa-chalkboard-user / fa-route).",
                              "example": "fa-circle-play"
                            },
                            "category": {
                              "type": "object",
                              "nullable": true,
                              "properties": {
                                "name": {
                                  "type": "string",
                                  "example": "Safety"
                                },
                                "color": {
                                  "type": "string",
                                  "nullable": true,
                                  "description": "Category color (hex).",
                                  "example": "#28a745"
                                },
                                "icon": {
                                  "type": "string",
                                  "nullable": true,
                                  "description": "Font Awesome glyph.",
                                  "example": "fa-shield-alt"
                                }
                              }
                            },
                            "hero_image_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Absolute URL of a resized hero image, or null.",
                              "example": "https://acme.workforce.mangoapps.com/rails/active_storage/..."
                            }
                          }
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible (`access_denied`); the token lacks\nthe `write:training` scope (`insufficient_permissions`); or\nself-enrollment is off (`self_enrollment_disabled`) — the message\nnames WHICH switch, since one is a tenant setting an admin controls\nand the other is a property of the course or path.\n"
          },
          "404": {
            "description": "No such course or learning path, or it is not visible to this caller (`not_found`)."
          },
          "422": {
            "description": "Prerequisites are not met (`prerequisites_not_met`, courses only —\n`error.details.missing_prerequisites` lists the blocking courses), or\nthe enrollment could not be written (`enrollment_failed`).\n"
          }
        }
      }
    },
    "/training/learning_paths/{learning_path_id}/enroll": {
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Self-enroll in a learning path",
        "description": "The action behind the catalog's own `enroll` and `start_path` CTAs\n(`cta_action` on a catalog card), which named an operation that had no\nnative endpoint until now.\n\nONE operation, two routes — `/training/courses/{course_id}/enroll` and\n`/training/learning_paths/{learning_path_id}/enroll` — resolved from\nwhichever id the route carried, the same shape the reviews and Q&A\nwrites use. No request body.\n\n**IDEMPOTENT.** A repeat (double-tapped button, retried request on a\nflaky connection) returns `200` with `created: false` and\n`already_enrolled: true`, NOT an error — so a client may retry safely.\n`created` is the flag to branch on; the accompanying `message` is\nalready worded for either case.\n\n**Both self-enrollment switches must be on**, exactly as every\nlearner-facing surface reads them: the tenant-wide Training setting AND\nthe per-subject admin toggle. Note the two subject defaults differ — a\ncourse allows self-enrollment unless an admin turns it off, a learning\npath denies it unless an admin turns it on — so the same tenant can\nlegitimately answer 200 for a course and 403 for a path.\n\n**Courses additionally enforce prerequisites**; learning paths have no\nprerequisites concept, so that refusal cannot occur for a path. The 422\ncarries the blocking courses in `error.details.missing_prerequisites`\nso the client can name them rather than render a bare refusal.\n\nEnrolling in a PATH creates only the path enrollment — its member\ncourses enroll lazily when the learner opens them, so do not expect\ncourse rows to appear in `/training/my_training/enrollments` yet.\n\nThe returned `enrollment` is the same `TrainingCard` the My Training\nlist renders, so a client can insert it into that list without a\nrefetch.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "learning_path_id",
            "in": "path",
            "required": true,
            "description": "The learning path to start. Digit-constrained.",
            "schema": {
              "type": "integer",
              "example": 6
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Enrolled, or already enrolled — branch on `created`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "created",
                    "already_enrolled",
                    "message",
                    "enrollment"
                  ],
                  "properties": {
                    "created": {
                      "type": "boolean",
                      "description": "True when this request created the enrollment; false on an idempotent repeat.",
                      "example": true
                    },
                    "already_enrolled": {
                      "type": "boolean",
                      "description": "The inverse of `created`, for a client that reads the state rather than the event.",
                      "example": false
                    },
                    "message": {
                      "type": "string",
                      "description": "Learner-facing copy, already worded for the created / already-enrolled case.",
                      "example": "Successfully enrolled!"
                    },
                    "enrollment": {
                      "type": "object",
                      "description": "One course or path enrollment card. `type` selects which progress fields are present.",
                      "required": [
                        "id",
                        "type",
                        "status",
                        "in_progress",
                        "assigned",
                        "completed",
                        "overdue",
                        "progress_percentage",
                        "subject"
                      ],
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 4412
                        },
                        "type": {
                          "type": "string",
                          "enum": [
                            "course",
                            "path"
                          ],
                          "example": "course"
                        },
                        "status": {
                          "type": "string",
                          "description": "Raw enrollment status column.",
                          "enum": [
                            "enrolled",
                            "in_progress",
                            "completed",
                            "cancelled"
                          ],
                          "example": "in_progress"
                        },
                        "status_key": {
                          "type": "string",
                          "description": "Learner-facing state (Training::Display) — note a raw `enrolled` reads as not_started, exactly as the web badge does.",
                          "enum": [
                            "not_started",
                            "in_progress",
                            "completed",
                            "registered"
                          ],
                          "example": "in_progress"
                        },
                        "status_label": {
                          "type": "string",
                          "description": "Display label for status_key (In Progress / Not Started / Completed / Registered).",
                          "example": "In Progress"
                        },
                        "in_progress": {
                          "type": "boolean",
                          "example": true
                        },
                        "assigned": {
                          "type": "boolean",
                          "description": "Assigned by an admin/manager/rule (renders the ASSIGNED chip).",
                          "example": true
                        },
                        "completed": {
                          "type": "boolean",
                          "example": false
                        },
                        "overdue": {
                          "type": "boolean",
                          "example": false
                        },
                        "progress_percentage": {
                          "type": "integer",
                          "description": "0–100.",
                          "example": 50
                        },
                        "due_date": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "example": "2026-08-15T00:00:00Z"
                        },
                        "days_overdue": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Present only when overdue.",
                          "example": null
                        },
                        "completed_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "example": null
                        },
                        "last_accessed_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Courses only.",
                          "example": "2026-08-12T14:03:00Z"
                        },
                        "completed_lessons_count": {
                          "type": "integer",
                          "description": "Course cards only. Counted WITHIN `total_lessons` — completions for lessons outside the learner's pinned version are excluded, so this can never exceed the total.",
                          "example": 3
                        },
                        "total_lessons": {
                          "type": "integer",
                          "description": "Course cards only. The lesson count of the version the learner is PINNED to (falling back to the current published version when not yet pinned) — not the course's lifetime lesson count across every version. Matches the row count of `/training/courses/{id}/lessons` for the same learner.",
                          "example": 6
                        },
                        "next_lesson": {
                          "type": "object",
                          "nullable": true,
                          "description": "Course cards only, and only in Continue-Learning — the lesson to resume into.",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 91
                            },
                            "title": {
                              "type": "string",
                              "example": "Lockout/Tagout Procedure"
                            }
                          }
                        },
                        "completed_steps": {
                          "type": "integer",
                          "description": "Path cards only.",
                          "example": 1
                        },
                        "total_steps": {
                          "type": "integer",
                          "description": "Path cards only.",
                          "example": 3
                        },
                        "total_courses": {
                          "type": "integer",
                          "description": "Path cards only — courses in the path.",
                          "example": 8
                        },
                        "current_step": {
                          "type": "integer",
                          "description": "Path cards only — 1-based \"Step N of M\" (caps at total_steps).",
                          "example": 2
                        },
                        "subject": {
                          "type": "object",
                          "nullable": true,
                          "description": "The course or path. Null for a hidden SYSTEM course (\"course unavailable\").",
                          "required": [
                            "id",
                            "title",
                            "type",
                            "delivery_label"
                          ],
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 320
                            },
                            "title": {
                              "type": "string",
                              "example": "Workplace Safety Fundamentals"
                            },
                            "slug": {
                              "type": "string",
                              "nullable": true,
                              "example": "workplace-safety-fundamentals"
                            },
                            "type": {
                              "type": "string",
                              "enum": [
                                "course",
                                "path"
                              ],
                              "example": "course"
                            },
                            "delivery_label": {
                              "type": "string",
                              "description": "Self-paced / Instructor-Led / Learning Path.",
                              "enum": [
                                "Self-paced",
                                "Instructor-Led",
                                "Learning Path"
                              ],
                              "example": "Self-paced"
                            },
                            "delivery_icon": {
                              "type": "string",
                              "description": "Font Awesome glyph for the delivery type — the web banner's type fallback (fa-circle-play / fa-chalkboard-user / fa-route).",
                              "example": "fa-circle-play"
                            },
                            "category": {
                              "type": "object",
                              "nullable": true,
                              "properties": {
                                "name": {
                                  "type": "string",
                                  "example": "Safety"
                                },
                                "color": {
                                  "type": "string",
                                  "nullable": true,
                                  "description": "Category color (hex).",
                                  "example": "#28a745"
                                },
                                "icon": {
                                  "type": "string",
                                  "nullable": true,
                                  "description": "Font Awesome glyph.",
                                  "example": "fa-shield-alt"
                                }
                              }
                            },
                            "hero_image_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Absolute URL of a resized hero image, or null.",
                              "example": "https://acme.workforce.mangoapps.com/rails/active_storage/..."
                            }
                          }
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible (`access_denied`); the token lacks\nthe `write:training` scope (`insufficient_permissions`); or\nself-enrollment is off (`self_enrollment_disabled`) — the message\nnames WHICH switch, since one is a tenant setting an admin controls\nand the other is a property of the course or path.\n"
          },
          "404": {
            "description": "No such course or learning path, or it is not visible to this caller (`not_found`)."
          },
          "422": {
            "description": "Prerequisites are not met (`prerequisites_not_met`, courses only —\n`error.details.missing_prerequisites` lists the blocking courses), or\nthe enrollment could not be written (`enrollment_failed`).\n"
          }
        }
      }
    },
    "/training/courses/{course_id}/enrollment": {
      "delete": {
        "tags": [
          "Training"
        ],
        "summary": "Leave a self-enrolled course",
        "description": "**Requires the `write:training` scope.** No request body.\n\nThe learner's OWN withdrawal — the undo of `POST .../enroll`, and the\nnative twin of the web's `DELETE learner/courses/{id}/withdraw` and\n`DELETE learner/learning_paths/{id}/withdraw`. ONE operation, two\nroutes — `/training/courses/{course_id}/enrollment` and\n`/training/learning_paths/{learning_path_id}/enrollment` — resolved\nfrom whichever id the route carried, exactly as the enroll pair is.\n\n**DELETE on the ENROLLMENT sub-resource, not on the course** (the same\nshape as `DELETE /training/sessions/{id}/registration`): the course is\nnot being removed, the caller's enrollment in it is. Only the caller's\nown CURRENT enrollment is ever touched — there is no way to name\nsomebody else's.\n\n**Deliberately narrow, because Training is the compliance system of\nrecord.** The rule is `Training::EnrollmentCancellationService\n.withdrawal_refusal`, the same one the web's withdraw actions and their\nsidebar buttons read, so mobile and web cannot disagree on who may\nleave what. It refuses, with `403 withdrawal_refused` and a\nlearner-facing message naming why:\n\n* anything ASSIGNED — by a person, a training assignment, an automation\n  rule, or a covering learning path (`enrollment.assigned` on the\n  course detail / `assigned` on the card is the hint to hide the\n  control) — \"only an administrator can remove it\";\n* a completed enrollment, or one holding a certificate — permanent\n  history;\n* any enrollment when the tenant has self-enrollment turned off.\n\nA course withdrawal CLEARS the learner's lesson completions and quiz\nattempts on it (a self-paced course) or RELEASES the seat with waitlist\npromotion (an instructor-led one); the cancelled row is kept for audit.\nBoth are irreversible — confirm before calling. Leaving a PATH does not\ncancel the member courses already started from it: those stay in\nMy Training (and, having come from the path, cannot be dropped\nindividually either), and the `message` says so with the count.\n\nAfter a successful withdrawal the learner may enroll again through\n`POST .../enroll` — the cancelled attempt is retired, not left blocking.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "description": "The course to leave. Digit-constrained.",
            "schema": {
              "type": "integer",
              "example": 128
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Withdrawn. `message` is learner-facing copy already worded for the delivery mode / kept-courses case.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "withdrawn",
                    "message",
                    "enrollment"
                  ],
                  "properties": {
                    "withdrawn": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "You've been removed from \"Forklift Refresher\". Any progress you'd made on it has been cleared."
                    },
                    "enrollment": {
                      "type": "object",
                      "description": "The cancelled row, so a client holding the My Training card it came from can drop that card without a refetch.",
                      "required": [
                        "id",
                        "status"
                      ],
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 4412
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "cancelled"
                          ],
                          "example": "cancelled"
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible (`access_denied`); the token lacks\nthe `write:training` scope (`insufficient_permissions`); or the\nlearner may not withdraw from THIS enrollment (`withdrawal_refused`)\n— it was assigned, is completed or certificated, or self-enrollment\nis off for the tenant. The message names which, in learner-facing\ncopy identical to the web's.\n"
          },
          "404": {
            "description": "No such course or learning path, or it is not visible to this caller (`not_found`)."
          },
          "422": {
            "description": "The caller holds no current enrollment in it (`not_enrolled`), or the\ncancellation could not be written (`withdrawal_failed`).\n"
          }
        }
      }
    },
    "/training/learning_paths/{learning_path_id}/enrollment": {
      "delete": {
        "tags": [
          "Training"
        ],
        "summary": "Leave a self-enrolled learning path",
        "description": "**Requires the `write:training` scope.** No request body.\n\nThe learner's OWN withdrawal — the undo of `POST .../enroll`, and the\nnative twin of the web's `DELETE learner/courses/{id}/withdraw` and\n`DELETE learner/learning_paths/{id}/withdraw`. ONE operation, two\nroutes — `/training/courses/{course_id}/enrollment` and\n`/training/learning_paths/{learning_path_id}/enrollment` — resolved\nfrom whichever id the route carried, exactly as the enroll pair is.\n\n**DELETE on the ENROLLMENT sub-resource, not on the course** (the same\nshape as `DELETE /training/sessions/{id}/registration`): the course is\nnot being removed, the caller's enrollment in it is. Only the caller's\nown CURRENT enrollment is ever touched — there is no way to name\nsomebody else's.\n\n**Deliberately narrow, because Training is the compliance system of\nrecord.** The rule is `Training::EnrollmentCancellationService\n.withdrawal_refusal`, the same one the web's withdraw actions and their\nsidebar buttons read, so mobile and web cannot disagree on who may\nleave what. It refuses, with `403 withdrawal_refused` and a\nlearner-facing message naming why:\n\n* anything ASSIGNED — by a person, a training assignment, an automation\n  rule, or a covering learning path (`enrollment.assigned` on the\n  course detail / `assigned` on the card is the hint to hide the\n  control) — \"only an administrator can remove it\";\n* a completed enrollment, or one holding a certificate — permanent\n  history;\n* any enrollment when the tenant has self-enrollment turned off.\n\nA course withdrawal CLEARS the learner's lesson completions and quiz\nattempts on it (a self-paced course) or RELEASES the seat with waitlist\npromotion (an instructor-led one); the cancelled row is kept for audit.\nBoth are irreversible — confirm before calling. Leaving a PATH does not\ncancel the member courses already started from it: those stay in\nMy Training (and, having come from the path, cannot be dropped\nindividually either), and the `message` says so with the count.\n\nAfter a successful withdrawal the learner may enroll again through\n`POST .../enroll` — the cancelled attempt is retired, not left blocking.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "learning_path_id",
            "in": "path",
            "required": true,
            "description": "The learning path to leave. Digit-constrained.",
            "schema": {
              "type": "integer",
              "example": 6
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Withdrawn. `message` is learner-facing copy already worded for the delivery mode / kept-courses case.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "withdrawn",
                    "message",
                    "enrollment"
                  ],
                  "properties": {
                    "withdrawn": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "You've been removed from \"Forklift Refresher\". Any progress you'd made on it has been cleared."
                    },
                    "enrollment": {
                      "type": "object",
                      "description": "The cancelled row, so a client holding the My Training card it came from can drop that card without a refetch.",
                      "required": [
                        "id",
                        "status"
                      ],
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 4412
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "cancelled"
                          ],
                          "example": "cancelled"
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible (`access_denied`); the token lacks\nthe `write:training` scope (`insufficient_permissions`); or the\nlearner may not withdraw from THIS enrollment (`withdrawal_refused`)\n— it was assigned, is completed or certificated, or self-enrollment\nis off for the tenant. The message names which, in learner-facing\ncopy identical to the web's.\n"
          },
          "404": {
            "description": "No such course or learning path, or it is not visible to this caller (`not_found`)."
          },
          "422": {
            "description": "The caller holds no current enrollment in it (`not_enrolled`), or the\ncancellation could not be written (`withdrawal_failed`).\n"
          }
        }
      }
    },
    "/training/catalog": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "Course + learning-path catalog",
        "description": "The Catalog tab — browses published COURSES and LEARNING PATHS together\n(courses first, then paths), reconciled to the mobile prototype's controls:\na `type` segmented tab bar (with per-type `counts`), a `category` filter, a\n`duration` bucket, and `sort`. Paginated.\n\nCards are enrollment-aware (the prototype's Continue / Enroll / Register /\nStart-path CTAs): each carries `enrollment_status` (null when not enrolled)\nand a derived `cta_action`. A `type: course` card carries `lessons_count`\n(or `sessions_count` for instructor-led) / `rating` / `free` / `price`; a\n`type: path` card carries `steps_count` / `courses_count`. `counts` badges\nthe type tabs; `filters` echoes the applied values.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Free-text search over TITLE and DESCRIPTION, for courses AND paths, matched case-insensitively as a substring. Runs the models' own `search` scopes — the SAME predicate the web learner catalog and the `/m/` catalog run — so one term cannot return three different result sets across surfaces.\n\nNO RANKING, deliberately: results come back in the requested `sort` order, not by relevance, exactly as the web behaves. `sort=relevant` is the catalog's default order, not a relevance score.\n\nBlank or missing is a NO-OP, not an error — the full catalog comes back with a 200 — so a client may call this on every keystroke. A crafted `?q[]=x` is ignored the same way rather than searching for the literal text `[\"x\"]`.\n\nCOMPOSES WITH EVERY OTHER FILTER (AND), and it narrows `counts` — see there.",
            "schema": {
              "type": "string",
              "example": "safety"
            }
          },
          {
            "name": "type",
            "in": "query",
            "description": "Narrow to one delivery type. Blank or unrecognised = all (and `filters.type` reports null, never an unapplied value). Segmented tabs; see `counts`.",
            "schema": {
              "type": "string",
              "enum": [
                "self_paced",
                "instructor_led",
                "learning_path"
              ],
              "example": "self_paced"
            }
          },
          {
            "name": "category",
            "in": "query",
            "description": "A category NAME (case-insensitive; courses match the free-text column, paths match their TrainingCategory name). See /training/catalog/categories.",
            "schema": {
              "type": "string",
              "example": "Safety"
            }
          },
          {
            "name": "duration",
            "in": "query",
            "description": "Duration bucket over the displayed total (Under 30 min / 30–60 min / Over 1 hour). Items with an unknown/zero duration match no bucket.",
            "schema": {
              "type": "string",
              "enum": [
                "under_30",
                "30_60",
                "over_60"
              ],
              "example": "under_30"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "relevant (default; catalog order) · newest (published/created desc) · shortest (duration asc) · due_date (caller's enrollment due date asc, unenrolled last).",
            "schema": {
              "type": "string",
              "enum": [
                "relevant",
                "newest",
                "shortest",
                "due_date"
              ],
              "default": "relevant",
              "example": "relevant"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Catalog retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "filters",
                    "counts",
                    "meta"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "A course or path browse tile. `type` selects which fields are present.",
                        "required": [
                          "id",
                          "type",
                          "title",
                          "delivery_label",
                          "enrollment_status",
                          "enrolled",
                          "cta_action"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 320
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "course",
                              "path"
                            ],
                            "example": "course"
                          },
                          "title": {
                            "type": "string",
                            "example": "Workplace Safety Fundamentals"
                          },
                          "slug": {
                            "type": "string",
                            "nullable": true,
                            "example": "workplace-safety-fundamentals"
                          },
                          "description": {
                            "type": "string",
                            "nullable": true,
                            "description": "Plain-text snippet (HTML stripped, truncated to 120 chars — the same rule and length as the web cards).",
                            "example": "Core safety practices for the floor."
                          },
                          "delivery_label": {
                            "type": "string",
                            "description": "Self-paced / Instructor-Led / Learning Path.",
                            "enum": [
                              "Self-paced",
                              "Instructor-Led",
                              "Learning Path"
                            ],
                            "example": "Self-paced"
                          },
                          "delivery_icon": {
                            "type": "string",
                            "description": "Font Awesome glyph for the delivery type — the web banner's type fallback (fa-circle-play / fa-chalkboard-user / fa-route).",
                            "example": "fa-circle-play"
                          },
                          "category": {
                            "type": "object",
                            "nullable": true,
                            "description": "The subject's TrainingCategory - name, hex color and Font Awesome glyph - for a COURSE and a path alike. Falls back to a legacy free-text course category (name only, null color/icon) when no category record is linked, and is null when the subject has neither.",
                            "properties": {
                              "name": {
                                "type": "string",
                                "example": "Safety"
                              },
                              "color": {
                                "type": "string",
                                "nullable": true,
                                "example": "#28a745"
                              },
                              "icon": {
                                "type": "string",
                                "nullable": true,
                                "example": "fa-shield-alt"
                              }
                            }
                          },
                          "hero_image_url": {
                            "type": "string",
                            "nullable": true,
                            "description": "Absolute URL of a resized hero image, or null.",
                            "example": "https://acme.workforce.mangoapps.com/rails/active_storage/..."
                          },
                          "formatted_duration": {
                            "type": "string",
                            "nullable": true,
                            "example": "1h 30m"
                          },
                          "enrollment_status": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "enrolled",
                              "in_progress",
                              "completed",
                              "cancelled"
                            ],
                            "description": "The caller's raw enrollment status, or null when not enrolled. `cancelled` falls through to an enrol-style cta_action, exactly as if unenrolled.",
                            "example": null
                          },
                          "enrolled": {
                            "type": "boolean",
                            "example": false
                          },
                          "cta_action": {
                            "type": "string",
                            "enum": [
                              "enroll",
                              "register",
                              "start_path",
                              "continue",
                              "buy",
                              "view",
                              "locked"
                            ],
                            "description": "Derived button action (client maps to a label). `locked` means the caller may not act on this item from the catalog — render NO CTA (the same value `TrainingDetailCta.action` carries): a learning path or course with self-enrollment off, an instructor-led course not offering registration, or a priced course on a tenant without commerce. The set is exactly what `CatalogCardSerializer#cta_action` returns; a closed enum in a generated client fails the whole response on an unknown value, so every value the server can send is listed here.",
                            "example": "enroll"
                          },
                          "lessons_count": {
                            "type": "integer",
                            "description": "Course cards only.",
                            "example": 6
                          },
                          "sessions_count": {
                            "type": "integer",
                            "description": "Instructor-led course cards only — BOOKABLE sessions (scheduled and still upcoming), the same number the course detail reports as sessions_available. Cancelled and past dates are excluded.",
                            "example": 7
                          },
                          "rating": {
                            "type": "object",
                            "description": "Course cards only.",
                            "properties": {
                              "average": {
                                "type": "number",
                                "nullable": true,
                                "example": 4.5
                              },
                              "count": {
                                "type": "integer",
                                "example": 12
                              }
                            }
                          },
                          "free": {
                            "type": "boolean",
                            "description": "Course cards only.",
                            "example": true
                          },
                          "price": {
                            "type": "string",
                            "description": "Course cards only — the formatted cost, always present: the price when the course is paid, \"Free\" when it is not. Branch on `free` for logic; `price` is display text either way.",
                            "example": "$49.00"
                          },
                          "steps_count": {
                            "type": "integer",
                            "description": "Path cards only.",
                            "example": 3
                          },
                          "courses_count": {
                            "type": "integer",
                            "description": "Path cards only.",
                            "example": 8
                          }
                        }
                      }
                    },
                    "filters": {
                      "type": "object",
                      "description": "The applied filter values (null when not set).",
                      "properties": {
                        "q": {
                          "type": "string",
                          "nullable": true,
                          "description": "The applied search term",
                          "or null. Echoed so a client can badge it and tell \"filtered and empty\" apart from \"my term was ignored\" — a crafted non-scalar `q` comes back null.": null,
                          "example": null
                        },
                        "type": {
                          "type": "string",
                          "nullable": true,
                          "example": null
                        },
                        "category": {
                          "type": "string",
                          "nullable": true,
                          "example": null
                        },
                        "duration": {
                          "type": "string",
                          "nullable": true,
                          "enum": [
                            "under_30",
                            "30_60",
                            "over_60"
                          ],
                          "example": null
                        },
                        "sort": {
                          "type": "string",
                          "enum": [
                            "relevant",
                            "newest",
                            "shortest",
                            "due_date"
                          ],
                          "example": "relevant"
                        }
                      }
                    },
                    "counts": {
                      "type": "object",
                      "description": "Per-type tab counts over the set narrowed by `q` AND `category`, but NOT by `type` — so tapping a type tab does not change them, while searching or switching category does. Overlap-free: each item is exactly one type.\n\nEACH COUNT IS WHAT TAPPING THAT TAB RETURNS. That is why `q` is inside the count and `type` is outside it: `q` and `category` narrow what the learner is looking at, whereas `type` IS the tab that was tapped, and counting after it would leave every tab showing its own count and zero for its siblings. Concretely, with `?q=safety` on a 45-item catalog the counts come back all=5 / self_paced=3 / instructor_led=0 / learning_path=2 — and `?q=safety&type=instructor_led` really does return an empty list, which is what the 0 is telling the client in advance.",
                      "required": [
                        "all",
                        "self_paced",
                        "instructor_led",
                        "learning_path"
                      ],
                      "properties": {
                        "all": {
                          "type": "integer",
                          "example": 53
                        },
                        "self_paced": {
                          "type": "integer",
                          "example": 41
                        },
                        "instructor_led": {
                          "type": "integer",
                          "example": 5
                        },
                        "learning_path": {
                          "type": "integer",
                          "example": 7
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination over the merged (courses + paths) result (build_pagination_meta).",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "example": 53
                        },
                        "total_pages": {
                          "type": "integer",
                          "example": 3
                        },
                        "current_page": {
                          "type": "integer",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "example": 20
                        },
                        "has_next_page": {
                          "type": "boolean",
                          "example": true
                        },
                        "has_prev_page": {
                          "type": "boolean",
                          "example": false
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (error code `access_denied`)."
          }
        }
      }
    },
    "/training/catalog/categories": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "Catalog category filter options",
        "description": "The category filter options for the Catalog tab — the case-insensitive\nunion of course (free-text) and path (TrainingCategory) categories, each\nwith a `count` of matching published items and, when a TrainingCategory\nbacks the name, its `color` / `icon`. Sorted alphabetically.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Categories retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "categories"
                  ],
                  "properties": {
                    "categories": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "name",
                          "value",
                          "count"
                        ],
                        "properties": {
                          "name": {
                            "type": "string",
                            "description": "Display name.",
                            "example": "Safety"
                          },
                          "value": {
                            "type": "string",
                            "description": "Pass as the catalog `category` param.",
                            "example": "Safety"
                          },
                          "count": {
                            "type": "integer",
                            "description": "Published courses + paths in this category.",
                            "example": 7
                          },
                          "color": {
                            "type": "string",
                            "nullable": true,
                            "example": "#28a745"
                          },
                          "icon": {
                            "type": "string",
                            "nullable": true,
                            "example": "fa-shield-alt"
                          }
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (error code `access_denied`)."
          }
        }
      }
    },
    "/training/my_records/certificates": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "My Records — certificates",
        "description": "The learner's certificates, the native mirror of the web My Certificates\nscreen (`Apps::Training::Learner::CertificatesController#index`), newest\nissued first and paginated.\n\nA **Certificate of Completion never expires** — it is permanent proof — so\n`summary.held` is simply the total, and a certificate's `status` reads\n`proof_only` unless it is linked to a certification (via the skill the\ncourse awarded), which is where any expiry clock lives. The status wording\ncomes from `Training::Display`, the same source the web validity chip\nreads, so the chip and the native badge can never disagree.\n\nSCOPE: this serves `TrainingCertificate` rows only. The web screen also\nmerges externally-recorded credentials (`EmployeeSkill`, source\n`external`); every row here carries `source: \"training\"` so adding those\nlater is additive rather than a breaking change.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Certificates retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "certificates",
                    "summary",
                    "meta"
                  ],
                  "properties": {
                    "certificates": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "source",
                          "certificate_number",
                          "title",
                          "status"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 64
                          },
                          "source": {
                            "type": "string",
                            "enum": [
                              "training"
                            ],
                            "description": "Always \"training\" today; the discriminator exists for future external credentials.",
                            "example": "training"
                          },
                          "certificate_number": {
                            "type": "string",
                            "description": "Human-quotable id, format CERT-YYYYMMDD-XXXXXX.",
                            "example": "CERT-20260803-BPAWWC"
                          },
                          "title": {
                            "type": "string",
                            "description": "The course or path the certificate was earned for.",
                            "example": "Benefits & Policies"
                          },
                          "issuer": {
                            "type": "string",
                            "nullable": true,
                            "description": "The issuing business.",
                            "example": "OfficeChat"
                          },
                          "issued_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "example": "2026-08-03T16:38:11Z"
                          },
                          "issued_at_label": {
                            "type": "string",
                            "nullable": true,
                            "description": "Pre-formatted issue date, matching the web.",
                            "example": "August 03",
                            "2026": null
                          },
                          "credits": {
                            "type": "number",
                            "nullable": true,
                            "description": "CE credits (frozen snapshot); null when zero so a client renders an em dash.",
                            "example": 1.5
                          },
                          "status": {
                            "type": "object",
                            "description": "Validity chip. `proof_only` = a completion certificate with no linked certification, which never expires.",
                            "required": [
                              "key",
                              "label",
                              "color"
                            ],
                            "properties": {
                              "key": {
                                "type": "string",
                                "enum": [
                                  "proof_only",
                                  "valid",
                                  "expiring",
                                  "expired"
                                ],
                                "example": "proof_only"
                              },
                              "label": {
                                "type": "string",
                                "description": "Display string, including the day count when expiring.",
                                "example": "Proof only"
                              },
                              "color": {
                                "type": "string",
                                "enum": [
                                  "secondary",
                                  "success",
                                  "warning",
                                  "danger"
                                ],
                                "example": "secondary"
                              },
                              "days_until_expiration": {
                                "type": "integer",
                                "nullable": true,
                                "description": "Present only when the key is `expiring`.",
                                "example": null
                              }
                            }
                          },
                          "expires_at": {
                            "type": "string",
                            "format": "date",
                            "nullable": true,
                            "description": "From the LINKED certification; null for proof-only certificates.",
                            "example": null
                          },
                          "certification_name": {
                            "type": "string",
                            "nullable": true,
                            "description": "Name of the linked certification (the skill's name, else the issuing authority).",
                            "example": null
                          },
                          "subject": {
                            "type": "object",
                            "nullable": true,
                            "description": "The course or path. Resolved SYSTEM-safely, so a tenant-hidden course still names the row.",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 60
                              },
                              "type": {
                                "type": "string",
                                "enum": [
                                  "course",
                                  "path"
                                ],
                                "example": "course"
                              },
                              "title": {
                                "type": "string",
                                "example": "Benefits & Policies"
                              },
                              "delivery_label": {
                                "type": "string",
                                "enum": [
                                  "Self-paced",
                                  "Instructor-Led",
                                  "Learning Path"
                                ],
                                "example": "Self-paced"
                              },
                              "category": {
                                "type": "object",
                                "nullable": true,
                                "properties": {
                                  "name": {
                                    "type": "string",
                                    "example": "onboarding"
                                  },
                                  "color": {
                                    "type": "string",
                                    "nullable": true,
                                    "example": null
                                  },
                                  "icon": {
                                    "type": "string",
                                    "nullable": true,
                                    "example": null
                                  }
                                }
                              }
                            }
                          },
                          "enrollment_id": {
                            "type": "integer",
                            "nullable": true,
                            "description": "The enrollment this certificate came from; matches a transcript entry's id (the detail screen's See this on my transcript link).",
                            "example": 22750
                          },
                          "course_version": {
                            "type": "object",
                            "nullable": true,
                            "description": "The pinned course version, rendered on the detail screen as \"v4 · Jul 2026\". Courses only — a path pins no version.",
                            "properties": {
                              "label": {
                                "type": "string",
                                "example": "v4"
                              },
                              "released_at": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true,
                                "example": "2026-07-01T00:00:00Z"
                              }
                            }
                          },
                          "verification_url": {
                            "type": "string",
                            "nullable": true,
                            "description": "Public, unauthenticated verification page for this certificate number.",
                            "example": "https://app.workforce.mangoapps.com/verify/certificate/CERT-20260803-BPAWWC"
                          },
                          "download_url": {
                            "type": "string",
                            "nullable": true,
                            "description": "Absolute URL of the certificate PDF; null when the PDF has not been generated yet.",
                            "example": "https://acme.workforce.mangoapps.com/rails/active_storage/blobs/..."
                          },
                          "thumbnail_url": {
                            "type": "string",
                            "nullable": true,
                            "description": "Preview image of the PDF's first page (what the web card shows).",
                            "example": "https://acme.workforce.mangoapps.com/rails/active_storage/representations/..."
                          }
                        }
                      }
                    },
                    "summary": {
                      "type": "object",
                      "description": "The header tiles, computed over the learner's WHOLE set (never the current page, which would show different numbers on page 2).",
                      "required": [
                        "held",
                        "active",
                        "expiring_soon",
                        "expired",
                        "completions"
                      ],
                      "properties": {
                        "held": {
                          "type": "integer",
                          "description": "Total certificates held (completion proof never expires, so this is the whole count).",
                          "example": 3
                        },
                        "active": {
                          "type": "integer",
                          "description": "Certificates whose linked certification has not expired.",
                          "example": 3
                        },
                        "expiring_soon": {
                          "type": "integer",
                          "description": "Linked certifications expiring within 30 days.",
                          "example": 0
                        },
                        "expired": {
                          "type": "integer",
                          "description": "Linked certifications already expired.",
                          "example": 0
                        },
                        "completions": {
                          "type": "integer",
                          "description": "The sibling Transcript tab's badge, so either endpoint can paint the whole tab bar.",
                          "example": 8
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "example": 3
                        },
                        "total_pages": {
                          "type": "integer",
                          "example": 1
                        },
                        "current_page": {
                          "type": "integer",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "example": 20
                        },
                        "has_next_page": {
                          "type": "boolean",
                          "example": false
                        },
                        "has_prev_page": {
                          "type": "boolean",
                          "example": false
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (error code `access_denied`)."
          }
        }
      }
    },
    "/training/my_records/certificates/{id}": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "My Records — one certificate",
        "description": "A single certificate — the Certificate Detail screen. Resolved through the\nsame learner-scoped loader as the list, so another learner's (or another\ntenant's) certificate returns 404 rather than leaking.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 64
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Certificate retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "certificate"
                  ],
                  "properties": {
                    "certificate": {
                      "type": "object",
                      "required": [
                        "id",
                        "source",
                        "certificate_number",
                        "title",
                        "status"
                      ],
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 64
                        },
                        "source": {
                          "type": "string",
                          "enum": [
                            "training"
                          ],
                          "description": "Always \"training\" today; the discriminator exists for future external credentials.",
                          "example": "training"
                        },
                        "certificate_number": {
                          "type": "string",
                          "description": "Human-quotable id, format CERT-YYYYMMDD-XXXXXX.",
                          "example": "CERT-20260803-BPAWWC"
                        },
                        "title": {
                          "type": "string",
                          "description": "The course or path the certificate was earned for.",
                          "example": "Benefits & Policies"
                        },
                        "issuer": {
                          "type": "string",
                          "nullable": true,
                          "description": "The issuing business.",
                          "example": "OfficeChat"
                        },
                        "issued_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "example": "2026-08-03T16:38:11Z"
                        },
                        "issued_at_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Pre-formatted issue date, matching the web.",
                          "example": "August 03",
                          "2026": null
                        },
                        "credits": {
                          "type": "number",
                          "nullable": true,
                          "description": "CE credits (frozen snapshot); null when zero so a client renders an em dash.",
                          "example": 1.5
                        },
                        "status": {
                          "type": "object",
                          "description": "Validity chip. `proof_only` = a completion certificate with no linked certification, which never expires.",
                          "required": [
                            "key",
                            "label",
                            "color"
                          ],
                          "properties": {
                            "key": {
                              "type": "string",
                              "enum": [
                                "proof_only",
                                "valid",
                                "expiring",
                                "expired"
                              ],
                              "example": "proof_only"
                            },
                            "label": {
                              "type": "string",
                              "description": "Display string, including the day count when expiring.",
                              "example": "Proof only"
                            },
                            "color": {
                              "type": "string",
                              "enum": [
                                "secondary",
                                "success",
                                "warning",
                                "danger"
                              ],
                              "example": "secondary"
                            },
                            "days_until_expiration": {
                              "type": "integer",
                              "nullable": true,
                              "description": "Present only when the key is `expiring`.",
                              "example": null
                            }
                          }
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "description": "From the LINKED certification; null for proof-only certificates.",
                          "example": null
                        },
                        "certification_name": {
                          "type": "string",
                          "nullable": true,
                          "description": "Name of the linked certification (the skill's name, else the issuing authority).",
                          "example": null
                        },
                        "subject": {
                          "type": "object",
                          "nullable": true,
                          "description": "The course or path. Resolved SYSTEM-safely, so a tenant-hidden course still names the row.",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 60
                            },
                            "type": {
                              "type": "string",
                              "enum": [
                                "course",
                                "path"
                              ],
                              "example": "course"
                            },
                            "title": {
                              "type": "string",
                              "example": "Benefits & Policies"
                            },
                            "delivery_label": {
                              "type": "string",
                              "enum": [
                                "Self-paced",
                                "Instructor-Led",
                                "Learning Path"
                              ],
                              "example": "Self-paced"
                            },
                            "category": {
                              "type": "object",
                              "nullable": true,
                              "properties": {
                                "name": {
                                  "type": "string",
                                  "example": "onboarding"
                                },
                                "color": {
                                  "type": "string",
                                  "nullable": true,
                                  "example": null
                                },
                                "icon": {
                                  "type": "string",
                                  "nullable": true,
                                  "example": null
                                }
                              }
                            }
                          }
                        },
                        "enrollment_id": {
                          "type": "integer",
                          "nullable": true,
                          "description": "The enrollment this certificate came from; matches a transcript entry's id (the detail screen's See this on my transcript link).",
                          "example": 22750
                        },
                        "course_version": {
                          "type": "object",
                          "nullable": true,
                          "description": "The pinned course version, rendered on the detail screen as \"v4 · Jul 2026\". Courses only — a path pins no version.",
                          "properties": {
                            "label": {
                              "type": "string",
                              "example": "v4"
                            },
                            "released_at": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true,
                              "example": "2026-07-01T00:00:00Z"
                            }
                          }
                        },
                        "verification_url": {
                          "type": "string",
                          "nullable": true,
                          "description": "Public, unauthenticated verification page for this certificate number.",
                          "example": "https://app.workforce.mangoapps.com/verify/certificate/CERT-20260803-BPAWWC"
                        },
                        "download_url": {
                          "type": "string",
                          "nullable": true,
                          "description": "Absolute URL of the certificate PDF; null when the PDF has not been generated yet.",
                          "example": "https://acme.workforce.mangoapps.com/rails/active_storage/blobs/..."
                        },
                        "thumbnail_url": {
                          "type": "string",
                          "nullable": true,
                          "description": "Preview image of the PDF's first page (what the web card shows).",
                          "example": "https://acme.workforce.mangoapps.com/rails/active_storage/representations/..."
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (error code `access_denied`)."
          },
          "404": {
            "description": "No such certificate for this learner (error code `not_found`)."
          }
        }
      }
    },
    "/training/my_records/transcript": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "My Records — transcript",
        "description": "The learner's official record of completed training — courses AND learning\npaths merged, newest completion first — mirroring the web transcript page\n(`Apps::Training::Learner::TranscriptController#index`) and the transcript\nPDF, all three reading one shared loader.\n\nRows paginate, but `summary` stays on the FULL filtered set: `total_ceu`\ntherefore tracks `?range` and always equals the sum of every row in the\ncurrent view rather than just the page — exactly as the web tiles behave.\nEach row's `credits` is that item's own frozen CE-credit snapshot.\n\nPath rows carry no `version_label` or `score` (a path pins no version and\nruns no quiz); the web renders an em dash for both.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "range",
            "in": "query",
            "description": "Limit to the current calendar year. Unrecognised values are treated as `all`. Ignored when `year` is given.",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "year"
              ],
              "default": "all",
              "example": "all"
            }
          },
          {
            "name": "year",
            "in": "query",
            "description": "A specific calendar year (the mobile period picker). Wins over `range`; see `available_years` for the options that have records.",
            "schema": {
              "type": "integer",
              "example": 2026
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Transcript retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "entries",
                    "learner",
                    "range",
                    "summary",
                    "meta"
                  ],
                  "properties": {
                    "entries": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "type",
                          "completed_at"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer",
                            "description": "The enrollment id.",
                            "example": 22750
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "course",
                              "path"
                            ],
                            "example": "course"
                          },
                          "completed_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "example": "2026-08-03T16:38:11Z"
                          },
                          "year": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Completion year — the transcript groups rows under year headings.",
                            "example": 2026
                          },
                          "version_label": {
                            "type": "string",
                            "nullable": true,
                            "description": "Course rows only — the pinned course version.",
                            "example": "v1"
                          },
                          "score": {
                            "type": "number",
                            "nullable": true,
                            "description": "Course rows only — best quiz score (percent); null when no scored quiz.",
                            "example": null
                          },
                          "credits": {
                            "type": "number",
                            "nullable": true,
                            "description": "CE credits for this item; null when zero.",
                            "example": 1.5
                          },
                          "certificate_id": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Fetch via /my_records/certificates/:id; null when the completion issued no certificate.",
                            "example": 64
                          },
                          "subject": {
                            "type": "object",
                            "nullable": true,
                            "description": "Null when the course is gone or hidden — the web prints \"Course unavailable\" but keeps the completion on the record.",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 60
                              },
                              "type": {
                                "type": "string",
                                "enum": [
                                  "course",
                                  "path"
                                ],
                                "example": "course"
                              },
                              "title": {
                                "type": "string",
                                "example": "Benefits & Policies"
                              },
                              "delivery_label": {
                                "type": "string",
                                "enum": [
                                  "Self-paced",
                                  "Instructor-Led",
                                  "Learning Path"
                                ],
                                "example": "Self-paced"
                              },
                              "category": {
                                "type": "object",
                                "nullable": true,
                                "properties": {
                                  "name": {
                                    "type": "string",
                                    "example": "onboarding"
                                  },
                                  "color": {
                                    "type": "string",
                                    "nullable": true,
                                    "example": null
                                  },
                                  "icon": {
                                    "type": "string",
                                    "nullable": true,
                                    "example": null
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "learner": {
                      "type": "object",
                      "description": "Whose official record this is — the transcript header (avatar initials, name, and the \"role · location\" line).",
                      "properties": {
                        "name": {
                          "type": "string",
                          "example": "Marcus Delgado"
                        },
                        "initials": {
                          "type": "string",
                          "nullable": true,
                          "example": "MD"
                        },
                        "job_title": {
                          "type": "string",
                          "nullable": true,
                          "description": "This tenant's job title for the learner.",
                          "example": "Parts Counter Associate"
                        },
                        "location": {
                          "type": "string",
                          "nullable": true,
                          "example": "Store #142"
                        }
                      }
                    },
                    "range": {
                      "type": "string",
                      "enum": [
                        "all",
                        "year"
                      ],
                      "description": "The all-time / this-year toggle actually applied. Ignored when `year` is given.",
                      "example": "all"
                    },
                    "year": {
                      "type": "integer",
                      "nullable": true,
                      "description": "The specific period applied via ?year=, or null when unfiltered.",
                      "example": null
                    },
                    "available_years": {
                      "type": "array",
                      "description": "Years the learner actually completed something in, newest first — the period picker's options.",
                      "items": {
                        "type": "integer",
                        "example": 2026
                      }
                    },
                    "summary": {
                      "type": "object",
                      "description": "The web page's three tiles, over the full filtered set.",
                      "required": [
                        "completions",
                        "total_ceu",
                        "total_ceu_all_time",
                        "certificates_held"
                      ],
                      "properties": {
                        "completions": {
                          "type": "integer",
                          "description": "Rows in the current view (tracks the period filter).",
                          "example": 8
                        },
                        "total_ceu": {
                          "type": "number",
                          "description": "Sum of every row in the current view (tracks the period filter), rounded to 1dp.",
                          "example": 3.5
                        },
                        "total_ceu_all_time": {
                          "type": "number",
                          "description": "Lifetime CEU total, never period-filtered — the figure shown above the period picker.",
                          "example": 18.5
                        },
                        "certificates_held": {
                          "type": "integer",
                          "description": "All-time certificate count (not range-filtered).",
                          "example": 3
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "example": 8
                        },
                        "total_pages": {
                          "type": "integer",
                          "example": 1
                        },
                        "current_page": {
                          "type": "integer",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "example": 20
                        },
                        "has_next_page": {
                          "type": "boolean",
                          "example": false
                        },
                        "has_prev_page": {
                          "type": "boolean",
                          "example": false
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (error code `access_denied`)."
          }
        }
      }
    },
    "/training/courses/{id}": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "Course detail (self-paced or instructor-led)",
        "description": "One course's detail screen, the native mirror of the web learner course page\n(`Apps::Training::Learner::CoursesController#show`). SELF-PACED and\nINSTRUCTOR-LED share this one payload — `delivery_mode` tells the client\nwhich variant to render, exactly as the web branches on `instructor_led?`:\n\n* self-paced — a progress block, a `lessons` outline, and a resume CTA;\n* instructor-led — no progress block, a `sessions` list with capacity and\n  booking CTAs, the instructors, and an \"Attendance: Instructor-marked\" info\n  row (a constant of the ILT flow, since the instructor marks the roster).\n\n`tabs` is the web's tab allowlist verbatim: an ILT course shows\n`about / sessions / qa`, an enrolled self-paced course `about / lessons / qa`,\nand a self-paced course you are only browsing shows `about` alone.\n\n**Two deliberate differences from the web page, both because this is a GET:**\n\n* **No writes.** The web `#show` auto-enrols the learner when the course sits\n  inside an active learning path and calls `start!`, moving them from\n  enrolled to in_progress just for opening the page. A read endpoint does\n  neither — the client uses the actions that own those transitions.\n* **Prerequisites are reported, not withheld.** The web renders a\n  \"prerequisites required\" page INSTEAD of the course; this returns the\n  course with `prerequisites: { met: false, missing: [...] }` so a client can\n  render the same blocked screen without a second request.\n\n`info_rows` and `additional_info` are ORDERED label/value pairs, not fixed\nkeys: the \"Course Info\" card and the custom-field block are both\ntenant-configurable (`additional_info` is every custom field flagged\nshow-on-info-page that has a value), so a client renders what it is given\nrather than hardcoding a field list. `info_rows` arrives in the web Course\nInfo card's own order — Duration, Lessons, Difficulty, Credits,\nCertificate, Self-enroll — with the two rows the web card has no\nequivalent for (Due, Version) after them.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 320
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Course retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "course"
                  ],
                  "properties": {
                    "course": {
                      "type": "object",
                      "required": [
                        "id",
                        "type",
                        "title",
                        "delivery_mode",
                        "delivery_label",
                        "tabs",
                        "cta"
                      ],
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 320
                        },
                        "type": {
                          "type": "string",
                          "enum": [
                            "course"
                          ],
                          "example": "course"
                        },
                        "title": {
                          "type": "string",
                          "example": "Workplace Safety Fundamentals"
                        },
                        "slug": {
                          "type": "string",
                          "nullable": true,
                          "example": "workplace-safety-fundamentals"
                        },
                        "delivery_mode": {
                          "type": "string",
                          "enum": [
                            "self_paced",
                            "instructor_led"
                          ],
                          "example": "self_paced"
                        },
                        "delivery_label": {
                          "type": "string",
                          "enum": [
                            "Self-paced",
                            "Instructor-Led"
                          ],
                          "example": "Self-paced"
                        },
                        "delivery_icon": {
                          "type": "string",
                          "example": "fa-circle-play"
                        },
                        "category": {
                          "type": "object",
                          "nullable": true,
                          "description": "From the course's TrainingCategory record — the same colour and glyph the web renders. Null only for a course with no category linked.",
                          "properties": {
                            "name": {
                              "type": "string",
                              "example": "Health & Safety"
                            },
                            "color": {
                              "type": "string",
                              "nullable": true,
                              "example": "#28a745"
                            },
                            "icon": {
                              "type": "string",
                              "nullable": true,
                              "example": "fa-shield-alt"
                            }
                          }
                        },
                        "categories": {
                          "type": "array",
                          "description": "The free-text category split on commas — the hero renders one pill each.",
                          "items": {
                            "type": "string",
                            "example": "Safety"
                          }
                        },
                        "hero_image_url": {
                          "type": "string",
                          "nullable": true,
                          "example": "https://acme.workforce.mangoapps.com/rails/active_storage/..."
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "description": "HTML.",
                          "example": "<p>The safety baseline every team member needs.</p>"
                        },
                        "learning_objectives": {
                          "type": "string",
                          "nullable": true,
                          "description": "HTML — the What you'll be able to do block.",
                          "example": "<ul><li>Identify hazards</li></ul>"
                        },
                        "intro_video": {
                          "type": "object",
                          "nullable": true,
                          "description": "Present only when the course has an intro video (embed URL or an attached file). The two are mutually exclusive in practice and reported in SEPARATE fields: `embed_url` goes in an iframe/webview, `file_url` is media bytes for a native player.",
                          "properties": {
                            "embed_url": {
                              "type": "string",
                              "nullable": true,
                              "example": "https://www.youtube.com/embed/abc123"
                            },
                            "provider": {
                              "type": "string",
                              "nullable": true,
                              "example": "youtube"
                            },
                            "attached": {
                              "type": "boolean",
                              "example": false
                            },
                            "file_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Absolute, EXPIRING (1 hour) URL of the uploaded intro video, served inline. Non-null only when `attached` is true; re-fetch the course rather than caching it.",
                              "example": "https://acme.workforce.mangoapps.com/rails/active_storage/blobs/redirect/…/intro.mp4?disposition=inline"
                            },
                            "content_type": {
                              "type": "string",
                              "nullable": true,
                              "description": "MIME type of the uploaded file, for picking a player. Non-null only when `attached` is true.",
                              "example": "video/mp4"
                            }
                          }
                        },
                        "difficulty_level": {
                          "type": "string",
                          "nullable": true,
                          "enum": [
                            "beginner",
                            "intermediate",
                            "advanced"
                          ],
                          "example": "beginner"
                        },
                        "formatted_duration": {
                          "type": "string",
                          "nullable": true,
                          "description": "Duration of the lesson set in `lessons[]` — ONE version, the caller's pinned version when enrolled and the current published one when browsing. NOT the sum across every version the course has ever had.",
                          "example": "1h 05m"
                        },
                        "duration_minutes": {
                          "type": "integer",
                          "nullable": true,
                          "description": "The same figure as `formatted_duration`, in minutes.",
                          "example": 65
                        },
                        "lessons_count": {
                          "type": "integer",
                          "example": 6
                        },
                        "ce_credits": {
                          "type": "number",
                          "nullable": true,
                          "description": "null when zero.",
                          "example": 1.5
                        },
                        "requires_certificate": {
                          "type": "boolean",
                          "example": true
                        },
                        "certificate_validity_days": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Raw validity window. The screen's Renewal Cycle line comes from a custom field, not from this.",
                          "example": 365
                        },
                        "version": {
                          "type": "object",
                          "nullable": true,
                          "description": "The current published version.",
                          "properties": {
                            "label": {
                              "type": "string",
                              "example": "v4"
                            },
                            "released_at": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true,
                              "example": "2026-07-01T00:00:00Z"
                            }
                          }
                        },
                        "instructors": {
                          "type": "array",
                          "description": "Instructor-led only — the distinct instructors across the UPCOMING sessions (so a course whose sessions have all finished reports `[]`, matching `sessions_available`). Carries the same person shape as the reviews and Q&A payloads, so one avatar component renders all three.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 44
                              },
                              "name": {
                                "type": "string",
                                "example": "Maria Lopez"
                              },
                              "initials": {
                                "type": "string",
                                "nullable": true,
                                "description": "For a client that would rather draw its own avatar tile than load one.",
                                "example": "ML"
                              },
                              "job_title": {
                                "type": "string",
                                "nullable": true,
                                "example": "Regional Safety Trainer"
                              },
                              "avatar_url": {
                                "type": "string",
                                "description": "Absolute. Never null for a live user — falls back to a generated initials tile when no photo is uploaded, so it is safe to render unconditionally.",
                                "example": "https://…/avatar.png"
                              }
                            }
                          }
                        },
                        "rating": {
                          "type": "object",
                          "description": "Approved reviews only.",
                          "properties": {
                            "average": {
                              "type": "number",
                              "nullable": true,
                              "example": 4.6
                            },
                            "count": {
                              "type": "integer",
                              "example": 128
                            },
                            "distribution": {
                              "type": "object",
                              "description": "Star -> count, for the 5..1 histogram.",
                              "additionalProperties": {
                                "type": "integer"
                              },
                              "example": {
                                "5": 88,
                                "4": 27,
                                "3": 8,
                                "2": 3,
                                "1": 2
                              }
                            },
                            "reviews": {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/TrainingReviewRow"
                              }
                            }
                          }
                        },
                        "free": {
                          "type": "boolean",
                          "example": true
                        },
                        "price": {
                          "type": "string",
                          "description": "Formatted cost, always present — the price when paid, \"Free\" when not. Branch on `free` for logic.",
                          "example": "Free"
                        },
                        "allow_self_enrollment": {
                          "type": "boolean",
                          "example": true
                        },
                        "tabs": {
                          "type": "array",
                          "description": "The web tab allowlist: ILT -> about/sessions/qa; enrolled self-paced -> about/lessons/qa; browsing self-paced -> about.",
                          "items": {
                            "type": "string",
                            "enum": [
                              "about",
                              "lessons",
                              "sessions",
                              "qa"
                            ]
                          },
                          "example": [
                            "about",
                            "lessons",
                            "qa"
                          ]
                        },
                        "info_rows": {
                          "$ref": "#/components/schemas/TrainingInfoRows"
                        },
                        "additional_info": {
                          "type": "array",
                          "description": "Custom fields flagged show-on-info-page that hold a value, in the admin's order. Drives the \"Additional information\" block (e.g. Regulatory Body, Renewal Cycle, Course Code).",
                          "items": {
                            "type": "object",
                            "properties": {
                              "label": {
                                "type": "string",
                                "example": "Regulatory Body"
                              },
                              "value": {
                                "type": "string",
                                "example": "OSHA 1910"
                              },
                              "field_type": {
                                "type": "string",
                                "example": "text"
                              }
                            }
                          }
                        },
                        "enrollment": {
                          "type": "object",
                          "nullable": true,
                          "description": "The caller's enrollment, or null when only browsing.",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 4412
                            },
                            "status": {
                              "type": "string",
                              "enum": [
                                "enrolled",
                                "in_progress",
                                "completed",
                                "cancelled"
                              ],
                              "example": "in_progress"
                            },
                            "status_key": {
                              "type": "string",
                              "enum": [
                                "not_started",
                                "in_progress",
                                "completed",
                                "registered"
                              ],
                              "example": "in_progress"
                            },
                            "status_label": {
                              "type": "string",
                              "example": "In Progress"
                            },
                            "progress_percentage": {
                              "type": "integer",
                              "example": 50
                            },
                            "completed_lessons_count": {
                              "type": "integer",
                              "example": 3
                            },
                            "total_lessons": {
                              "type": "integer",
                              "example": 6
                            },
                            "formatted_time_left": {
                              "type": "string",
                              "nullable": true,
                              "example": "45m"
                            },
                            "formatted_time_spent": {
                              "type": "string",
                              "nullable": true,
                              "example": "20m"
                            },
                            "due_date": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true,
                              "example": "2026-08-15T00:00:00Z"
                            },
                            "overdue": {
                              "type": "boolean",
                              "example": false
                            },
                            "days_overdue": {
                              "type": "integer",
                              "nullable": true,
                              "example": null
                            },
                            "assigned": {
                              "type": "boolean",
                              "description": "The enrollment was given to the learner rather than self-chosen.",
                              "example": true
                            },
                            "completed_at": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true,
                              "example": null
                            },
                            "certificate_id": {
                              "type": "integer",
                              "nullable": true,
                              "example": null
                            }
                          }
                        },
                        "course_paths": {
                          "type": "array",
                          "description": "The learning paths this course counts toward FOR THE CALLER — the web page's \"Assigned via {path} (+N more)\" pill, as an array so a course in two paths is represented honestly. Strictly the caller's OWN active (enrolled / in_progress) path enrollments that include this course; a path the caller is not on, or has completed or left, never appears, and it is never \"every path that lists the course\". `[]` for a course reached directly. `id` is the PATH's, so each entry opens through `GET /training/learning_paths/{id}`.",
                          "items": {
                            "type": "object",
                            "required": [
                              "id",
                              "title"
                            ],
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 88
                              },
                              "title": {
                                "type": "string",
                                "example": "New Hire Safety Onboarding"
                              }
                            }
                          }
                        },
                        "prerequisites": {
                          "type": "object",
                          "description": "The web BLOCKS the page when required prerequisites are unmet; this reports the state instead.",
                          "required": [
                            "met",
                            "missing"
                          ],
                          "properties": {
                            "met": {
                              "type": "boolean",
                              "example": true
                            },
                            "missing": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 88
                                  },
                                  "title": {
                                    "type": "string",
                                    "example": "Safety Orientation"
                                  }
                                }
                              }
                            }
                          }
                        },
                        "lessons": {
                          "type": "array",
                          "description": "The outline the caller may see: an enrolled learner gets the version they are PINNED to, a browser the current published version. The same rows the Lessons tab endpoint returns.",
                          "items": {
                            "$ref": "#/components/schemas/TrainingLessonRow"
                          }
                        },
                        "sessions_available": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Instructor-led only — count of upcoming scheduled sessions.",
                          "example": 7
                        },
                        "sessions": {
                          "type": "array",
                          "nullable": true,
                          "description": "Instructor-led only.",
                          "items": {
                            "$ref": "#/components/schemas/TrainingSessionRow"
                          }
                        },
                        "my_registration": {
                          "$ref": "#/components/schemas/TrainingSessionRegistrationSummary"
                        },
                        "qa": {
                          "type": "object",
                          "properties": {
                            "questions_count": {
                              "type": "integer",
                              "example": 3
                            },
                            "unanswered_count": {
                              "type": "integer",
                              "example": 1
                            }
                          }
                        },
                        "can_review": {
                          "type": "boolean",
                          "description": "Enrolled and has not already reviewed.",
                          "example": true
                        },
                        "my_review_id": {
                          "type": "integer",
                          "nullable": true,
                          "example": null
                        },
                        "cta": {
                          "$ref": "#/components/schemas/TrainingDetailCta"
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (error code `access_denied`)."
          },
          "404": {
            "description": "No such course visible to this caller (error code `not_found`)."
          }
        }
      }
    },
    "/training/learning_paths/{id}": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "Learning-path detail",
        "description": "One learning path's detail screen, the native mirror of the web learner path\npage (`Apps::Training::Learner::LearningPathsController#show`). Same shape as\na course detail with `type: \"path\"`, plus the `steps` outline.\n\n**Progress is computed LIVE** from required work (`required_done` /\n`required_total` over the step weights) and never read from the enrollment's\ncached `progress_percentage`: a member course completed outside the path\nleaves that column stale, which is why the web mobile view recomputes it too.\n\nA step is `locked` only when the path is `sequential`, the caller is\nenrolled, and the step sits after the first incomplete one — the web's exact\nrule.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 3
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Learning path retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "learning_path"
                  ],
                  "properties": {
                    "learning_path": {
                      "type": "object",
                      "required": [
                        "id",
                        "type",
                        "title",
                        "tabs",
                        "cta"
                      ],
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 3
                        },
                        "type": {
                          "type": "string",
                          "enum": [
                            "path"
                          ],
                          "example": "path"
                        },
                        "title": {
                          "type": "string",
                          "example": "New Hire Safety Onboarding"
                        },
                        "slug": {
                          "type": "string",
                          "nullable": true,
                          "example": "new-hire-safety"
                        },
                        "delivery_mode": {
                          "type": "string",
                          "enum": [
                            "learning_path"
                          ],
                          "example": "learning_path"
                        },
                        "delivery_label": {
                          "type": "string",
                          "enum": [
                            "Learning Path"
                          ],
                          "example": "Learning Path"
                        },
                        "delivery_icon": {
                          "type": "string",
                          "example": "fa-route"
                        },
                        "category": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "name": {
                              "type": "string",
                              "example": "Onboarding"
                            },
                            "color": {
                              "type": "string",
                              "nullable": true,
                              "example": "#ffc107"
                            },
                            "icon": {
                              "type": "string",
                              "nullable": true,
                              "example": "fa-door-open"
                            }
                          }
                        },
                        "hero_image_url": {
                          "type": "string",
                          "nullable": true,
                          "example": null
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "example": "<p>The onboarding path every new floor team member completes.</p>"
                        },
                        "learning_objectives": {
                          "type": "string",
                          "nullable": true,
                          "example": null
                        },
                        "intro_video": {
                          "type": "object",
                          "nullable": true,
                          "description": "Same shape as the course detail's — see `file_url` there.",
                          "properties": {
                            "embed_url": {
                              "type": "string",
                              "nullable": true,
                              "example": null
                            },
                            "provider": {
                              "type": "string",
                              "nullable": true,
                              "example": null
                            },
                            "attached": {
                              "type": "boolean",
                              "example": false
                            },
                            "file_url": {
                              "type": "string",
                              "nullable": true,
                              "description": "Absolute",
                              "expiring (1 hour) URL of the uploaded intro video. Non-null only when `attached`.": null,
                              "example": null
                            },
                            "content_type": {
                              "type": "string",
                              "nullable": true,
                              "example": null
                            }
                          }
                        },
                        "difficulty_level": {
                          "type": "string",
                          "nullable": true,
                          "enum": [
                            "beginner",
                            "intermediate",
                            "advanced"
                          ],
                          "example": "beginner"
                        },
                        "formatted_duration": {
                          "type": "string",
                          "nullable": true,
                          "example": "6h 20m"
                        },
                        "steps_count": {
                          "type": "integer",
                          "example": 3
                        },
                        "courses_count": {
                          "type": "integer",
                          "example": 8
                        },
                        "ce_credits": {
                          "type": "number",
                          "nullable": true,
                          "example": 6.5
                        },
                        "requires_certificate": {
                          "type": "boolean",
                          "example": true
                        },
                        "sequential": {
                          "type": "boolean",
                          "description": "Steps unlock in order.",
                          "example": true
                        },
                        "rating": {
                          "type": "object",
                          "properties": {
                            "average": {
                              "type": "number",
                              "nullable": true,
                              "example": 4.4
                            },
                            "count": {
                              "type": "integer",
                              "example": 61
                            },
                            "distribution": {
                              "type": "object",
                              "additionalProperties": {
                                "type": "integer"
                              },
                              "example": {
                                "5": 34,
                                "4": 18
                              }
                            },
                            "reviews": {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/TrainingReviewRow"
                              }
                            }
                          }
                        },
                        "allow_self_enrollment": {
                          "type": "boolean",
                          "example": true
                        },
                        "tabs": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "enum": [
                              "about",
                              "steps",
                              "qa"
                            ]
                          },
                          "example": [
                            "about",
                            "steps",
                            "qa"
                          ]
                        },
                        "info_rows": {
                          "$ref": "#/components/schemas/TrainingInfoRows"
                        },
                        "additional_info": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "label": {
                                "type": "string",
                                "example": "Path Code"
                              },
                              "value": {
                                "type": "string",
                                "example": "ONB-SAF-01"
                              },
                              "field_type": {
                                "type": "string",
                                "example": "text"
                              }
                            }
                          }
                        },
                        "enrollment": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 50
                            },
                            "status": {
                              "type": "string",
                              "enum": [
                                "enrolled",
                                "in_progress",
                                "completed",
                                "cancelled"
                              ],
                              "example": "in_progress"
                            },
                            "status_key": {
                              "type": "string",
                              "enum": [
                                "not_started",
                                "in_progress",
                                "completed",
                                "registered"
                              ],
                              "example": "in_progress"
                            },
                            "status_label": {
                              "type": "string",
                              "example": "In Progress"
                            },
                            "progress_percentage": {
                              "type": "integer",
                              "description": "LIVE required-work percentage, not the cached column.",
                              "example": 60
                            },
                            "required_done": {
                              "type": "integer",
                              "example": 4
                            },
                            "required_total": {
                              "type": "integer",
                              "example": 8
                            },
                            "steps_complete": {
                              "type": "integer",
                              "example": 1
                            },
                            "steps_total": {
                              "type": "integer",
                              "example": 3
                            },
                            "due_date": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true,
                              "example": "2026-08-15T00:00:00Z"
                            },
                            "overdue": {
                              "type": "boolean",
                              "example": false
                            },
                            "days_overdue": {
                              "type": "integer",
                              "nullable": true,
                              "example": null
                            },
                            "assigned": {
                              "type": "boolean",
                              "example": true
                            },
                            "completed_at": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true,
                              "example": null
                            },
                            "certificate_id": {
                              "type": "integer",
                              "nullable": true,
                              "example": null
                            }
                          }
                        },
                        "steps": {
                          "type": "array",
                          "description": "The same rows the Steps tab endpoint returns.",
                          "items": {
                            "$ref": "#/components/schemas/TrainingPathStepRow"
                          }
                        },
                        "qa": {
                          "type": "object",
                          "properties": {
                            "questions_count": {
                              "type": "integer",
                              "example": 3
                            },
                            "unanswered_count": {
                              "type": "integer",
                              "example": 0
                            }
                          }
                        },
                        "can_review": {
                          "type": "boolean",
                          "example": false
                        },
                        "my_review_id": {
                          "type": "integer",
                          "nullable": true,
                          "example": null
                        },
                        "cta": {
                          "$ref": "#/components/schemas/TrainingDetailCta"
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (error code `access_denied`)."
          },
          "404": {
            "description": "No such learning path visible to this caller (error code `not_found`)."
          }
        }
      }
    },
    "/training/courses/{course_id}/lessons": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "Course lessons tab",
        "description": "The course detail's LESSONS TAB on its own — the native mirror of the web\ntab.\n\nThe detail endpoint (`GET /training/courses/{id}`) already inlines this\noutline. This exists so a client can open or REFRESH just the tab — after\nfinishing a lesson, most obviously — without refetching a detail payload\nthat also carries sessions, prerequisites, custom fields, reviews and the\nCTA. The rows are produced by the same serializer the detail uses, so the\ntwo can never disagree.\n\nWHICH OUTLINE YOU GET depends on enrollment: an enrolled learner sees the\ncourse version their enrollment is PINNED to, so a mid-course republish\nnever reshuffles the lessons under them; anyone else sees the currently\npublished version.\n\nNOT PAGINATED, deliberately. Unlike reviews and Q&A, an outline is bounded\nby how the course was authored — tens of rows, not thousands — and the tab\nrenders it whole, so paginating would buy a round trip and a scroll\nposition to manage for nothing.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 37
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Lesson outline retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "type": "object",
                      "properties": {
                        "subject": {
                          "$ref": "#/components/schemas/TrainingSubjectBlock"
                        },
                        "enrolled": {
                          "type": "boolean",
                          "description": "Whether the caller holds an enrollment. When false every row is `locked`, none is `current`, and `progress.completed_lessons_count` is 0 — the browsing state.",
                          "example": true
                        },
                        "progress": {
                          "type": "object",
                          "description": "The same two numbers the course detail reports, from the same two sources. The detail spells them differently — the total as `course.lessons_count`, and the pair again inside `course.enrollment`, which is absent when the caller is not enrolled. Here they are always present, so a client rendering only the tab never has to special-case the browsing state.",
                          "properties": {
                            "completed_lessons_count": {
                              "type": "integer",
                              "example": 3
                            },
                            "total_lessons": {
                              "type": "integer",
                              "example": 8
                            }
                          }
                        },
                        "lessons": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/TrainingLessonRow"
                          }
                        },
                        "unread_notification_count": {
                          "$ref": "#/components/schemas/UnreadNotificationCount"
                        },
                        "_meta": {
                          "$ref": "#/components/schemas/ResponseMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (error code `access_denied`)."
          },
          "404": {
            "description": "No such course visible to this caller (error code `not_found`) — unknown id, another tenant's course, or one that is not published."
          }
        }
      }
    },
    "/training/learning_paths/{learning_path_id}/steps": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "Learning-path steps tab",
        "description": "The path detail's STEPS TAB on its own — the native mirror of the web tab,\nand the structural twin of the course Lessons tab above.\n\nThe detail endpoint (`GET /training/learning_paths/{id}`) already inlines\nthese steps; this is what a client re-fetches after finishing a member\ncourse, without pulling the whole detail back down. Same serializer as the\ndetail, so the two agree by construction.\n\nREAD `sequential` BEFORE RENDERING A PADLOCK: a non-sequential path never\nlocks a step, so `locked` must not be inferred from position alone.\n\nNot paginated — a path has a handful of steps by construction.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "learning_path_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Steps retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "type": "object",
                      "properties": {
                        "subject": {
                          "$ref": "#/components/schemas/TrainingSubjectBlock"
                        },
                        "sequential": {
                          "type": "boolean",
                          "description": "Whether the path's steps unlock in order. What makes `locked` on a step row meaningful.",
                          "example": true
                        },
                        "enrolled": {
                          "type": "boolean",
                          "description": "Whether the caller holds the path enrollment. When false nothing is locked and every course CTA is `enroll_required`.",
                          "example": true
                        },
                        "progress": {
                          "type": "object",
                          "description": "The live required-work rollup, recomputed from completed COURSES rather than read from the enrollment's cached percentage — which goes stale when a member course is completed outside the path.",
                          "properties": {
                            "percentage": {
                              "type": "integer",
                              "example": 40
                            },
                            "required_done": {
                              "type": "integer",
                              "example": 2
                            },
                            "required_total": {
                              "type": "integer",
                              "example": 5
                            },
                            "total_courses": {
                              "type": "integer",
                              "example": 7
                            },
                            "steps_complete": {
                              "type": "integer",
                              "example": 1
                            },
                            "first_incomplete_index": {
                              "type": "integer",
                              "nullable": true,
                              "description": "Zero-based; null once every step is complete.",
                              "example": 1
                            }
                          }
                        },
                        "steps": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/TrainingPathStepRow"
                          }
                        },
                        "unread_notification_count": {
                          "$ref": "#/components/schemas/UnreadNotificationCount"
                        },
                        "_meta": {
                          "$ref": "#/components/schemas/ResponseMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (error code `access_denied`)."
          },
          "404": {
            "description": "No such learning path visible to this caller (error code `not_found`) — unknown id, another tenant's path, or one that is not published."
          }
        }
      }
    },
    "/training/courses/{course_id}/lessons/{lesson_id}/content": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "One lesson packaged for offline storage",
        "description": "ONE self-paced lesson as HTML the client can STORE ON THE DEVICE and render\nwith no connection, plus everything needed to make that HTML work without\nus.\n\nUse this to download a lesson, cache a lesson for offline, sync course\ncontent, take a course offline, or prefetch lessons before going out of\ncoverage.\n\nFOR THE ONLINE CASE YOU DO NOT NEED THIS. Load the row's `web_view_url` in\na WebView instead — that is the same lesson rendered by the same view, with\nnothing to store and nothing to rewrite.\n\nWHAT THE CLIENT STILL HAS TO DO, in order:\n\n1. Download every url in `offline.stylesheets` and rewrite the `<link>`\n   hrefs to the local copies. These are the SAME digested files the online\n   webview links, so an offline lesson is styled by byte-identical CSS.\n2. Parse `offline.html` for `img[src]` and for `url(...)` inside `style`,\n   download each, and rewrite to the local copy. `img` is the only media\n   tag the body can contain — the sanitiser strips iframe, video, script\n   and object — so there is nothing else in the markup to look for.\n3. Render `offline.document` / `offline.video` with the platform's own\n   viewer. They are lesson-level attachments, NOT part of the body, so no\n   amount of parsing the html would find them.\n\n`offline.document.url` AND `offline.video.url` ARE SIGNED AND EXPIRING.\nThey are for the download pass only — never store the html still pointing\nat them, or the lesson works today and 404s next week. Re-fetch this\nendpoint to re-issue them.\n\nCHECK `offline.offline_supported` FIRST. Three kinds of lesson can never\nwork offline and say so here rather than leaving each platform to derive\nit: `scorm` and `partner_course` (the runtime is on an external host,\nreached with a per-launch token, reporting progress by webhook) and an\nexternally-hosted video (a YouTube/Vimeo embed). For those, fall back to\n`lesson.web_view_url` while online.\n\nA READ — no `write:training` scope needed, like every other Training read.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 37
            }
          },
          {
            "name": "lesson_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 91
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Lesson content retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "type": "object",
                      "properties": {
                        "lesson": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/TrainingLessonRow"
                            }
                          ],
                          "description": "The same row shape the outline returns, so a client can render the stored lesson's header without keeping a second model. `current` is always false here — it is an outline-relative flag."
                        },
                        "offline": {
                          "$ref": "#/components/schemas/TrainingOfflineLesson"
                        },
                        "unread_notification_count": {
                          "$ref": "#/components/schemas/UnreadNotificationCount"
                        },
                        "_meta": {
                          "$ref": "#/components/schemas/ResponseMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (error code `access_denied`)."
          },
          "404": {
            "description": "No such course visible to this caller, or no such lesson IN THAT COURSE (error code `not_found`). A real lesson id under the wrong course_id is a 404, not a redirect."
          }
        }
      }
    },
    "/training/courses/{course_id}/lessons/{lesson_id}/progress": {
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Record progress on one lesson",
        "description": "Record progress on a single lesson — mark it complete, save a video\nposition, or add time spent.\n\nUse this to complete a lesson, mark a lesson done, save my place in a\nvideo, resume where I left off, or record study time.\n\nA SCORE CANNOT BE SENT HERE. A learner does not grade themselves, so this\nendpoint ignores `score` even if you include it; the field is read back on\n`TrainingLessonProgress` but is written only by SCORM/xAPI callbacks and\npartner-content sync.\n\nWrites the SAME `TrainingLessonCompletion` row the web writes\n(`learner/lessons#complete` and `#update_progress`) — no second notion of\n\"done\", so a learner who finishes on the phone is finished on the web, and\nthe course's own progress percentage updates from the same callback.\n\nTWO RULES, both chosen because an offline client replays writes:\n\n* COMPLETION IS MONOTONIC. `completed: true` completes; nothing here\n  un-completes. A stale queued write from before a reset can never revoke\n  a completion the learner earned, and a double-tap or a retried flush is a\n  no-op rather than a toggle. `completed_at` is not restamped on a replay.\n* VIDEO POSITION TAKES THE MAX. Two devices, or a queue flushed out of\n  order, must not rewind the learner: sending 40 after 120 keeps 120. To\n  move a learner BACKWARDS deliberately, the client tracks that locally —\n  this endpoint will not do it.\n\n`time_spent` is a DELTA for this sitting, not a total — it is added to what\nis stored. Send it and your figure is authoritative; omit it and the server\nrecords wall-clock time from first open to completion, which is what the\nweb records.\n\nRequires `write:training`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 37
            }
          },
          {
            "name": "lesson_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 91
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TrainingLessonProgressInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Progress recorded",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "type": "object",
                      "properties": {
                        "progress": {
                          "$ref": "#/components/schemas/TrainingLessonProgress"
                        },
                        "unread_notification_count": {
                          "$ref": "#/components/schemas/UnreadNotificationCount"
                        },
                        "_meta": {
                          "$ref": "#/components/schemas/ResponseMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible (`access_denied`), or the token lacks `write:training` (`insufficient_permissions`)."
          },
          "404": {
            "description": "No such course visible to this caller, or no such lesson in that course (`not_found`)."
          },
          "422": {
            "description": "`not_enrolled` — the caller holds no ACTIVE enrollment on the course, so there is nothing to record progress against. Enroll first (`POST /training/courses/{id}/enroll`).\n\n`invalid_progress` — a value was rejected by the model (a negative position or time). `progress_failed` — anything else; the write did not happen and may be retried."
          }
        }
      }
    },
    "/training/lessons/progress/sync": {
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Flush a batch of offline lesson progress",
        "description": "The OFFLINE FLUSH DOOR: a batch of the per-lesson writes above, for a client\nthat has been recording progress with no connection.\n\nUse this to sync offline progress, upload queued progress, flush the\noffline queue, or catch up after reconnecting.\n\nPER-ITEM RESULTS, NEVER ALL-OR-NOTHING — and a client must read them that\nway. A queue that fails as a unit is a queue that cannot be drained: one\nlesson deleted by an admin, or one course the learner was unenrolled from,\nwould block every other item behind it forever. Each entry reports its own\n`ok` plus an `error` code; drop what succeeded, and drop\n`lesson_not_found` / `course_not_found` / `not_enrolled` permanently\n(retrying will never succeed). Only `progress_failed` is worth re-queueing.\n\nEvery item goes through the SAME write as the single-lesson endpoint, so\nthe monotonic-completion and max-video-position rules hold identically —\nwhich is what makes flushing a queue twice safe.\n\nAT MOST 100 ITEMS per request; more is a `422`, not a truncation. Split the\nqueue client-side.\n\nRequires `write:training`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "items"
                ],
                "properties": {
                  "items": {
                    "type": "array",
                    "maxItems": 100,
                    "description": "The queued writes, in any order. Must be an array.",
                    "items": {
                      "allOf": [
                        {
                          "type": "object",
                          "required": [
                            "course_id",
                            "lesson_id"
                          ],
                          "properties": {
                            "course_id": {
                              "type": "integer",
                              "example": 37
                            },
                            "lesson_id": {
                              "type": "integer",
                              "example": 91
                            }
                          }
                        },
                        {
                          "$ref": "#/components/schemas/TrainingLessonProgressInput"
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Batch processed. A 200 does NOT mean every item succeeded — read `failed` and the per-item `ok`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "type": "object",
                      "properties": {
                        "synced": {
                          "type": "integer",
                          "description": "How many items were written.",
                          "example": 4
                        },
                        "failed": {
                          "type": "integer",
                          "description": "How many were rejected. Non-zero with a 200 is normal.",
                          "example": 1
                        },
                        "results": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "index": {
                                "type": "integer",
                                "description": "Position in the `items` array you sent — how to match a result back to a queue entry.",
                                "example": 2
                              },
                              "lesson_id": {
                                "type": "integer",
                                "nullable": true,
                                "example": 91
                              },
                              "ok": {
                                "type": "boolean",
                                "example": false
                              },
                              "error": {
                                "type": "string",
                                "nullable": true,
                                "enum": [
                                  "invalid_item",
                                  "course_not_found",
                                  "lesson_not_found",
                                  "not_enrolled",
                                  "invalid_progress",
                                  "progress_failed"
                                ],
                                "description": "Present only when `ok` is false. The first four are PERMANENT — drop the queue entry. `progress_failed` may be retried.",
                                "example": "not_enrolled"
                              },
                              "progress": {
                                "allOf": [
                                  {
                                    "$ref": "#/components/schemas/TrainingLessonProgress"
                                  }
                                ],
                                "description": "Present only when `ok` is true."
                              }
                            }
                          }
                        },
                        "unread_notification_count": {
                          "$ref": "#/components/schemas/UnreadNotificationCount"
                        },
                        "_meta": {
                          "$ref": "#/components/schemas/ResponseMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible (`access_denied`), or the token lacks `write:training` (`insufficient_permissions`)."
          },
          "422": {
            "description": "`invalid_items` — `items` was missing or not an array. `too_many_items` — more than 100 entries; `details` carries `limit` and `received`."
          }
        }
      }
    },
    "/training/courses/{course_id}/lessons/{lesson_id}/quiz": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "Quiz card (assessment entry point)",
        "description": "Everything the learner sees BEFORE opening an attempt: the quiz's shape\n(question count, passing bar, time limit, points), their attempt tallies\nand remaining pool, their standing result, any attempt still open, and the\nONE action to offer.\n\nThis is the payload behind the assessment row in a course outline\n(\"Safety Assessment · Quiz · 7 questions · Pass 80%\") and behind the launch\nscreen. Re-read it after submitting to refresh that row without refetching\nthe whole course detail.\n\n**HOW A NATIVE CLIENT REACHES A QUIZ.** The learner does not arrive here\nfrom a list — they arrive from the lesson WebView, and the seam is a URL you\nintercept:\n\n1. Open the lesson row's `web_view_url` (the bare `?embed=1` /m/ render).\n2. It shows the quiz card, whose controls are plain GET links to\n   `/m/apps/training/courses/{course_id}/lessons/{lesson_id}/quiz/launch`,\n   carrying `?intent=` and — where one exists — `&attempt_id=`.\n3. Match that PATH in the WebView, ignoring the query string (`embed=1` and\n   `mobile=1` also ride along), cancel the navigation, open your own screen.\n4. **`intent` says which screen to open**, so no probe request is needed. It\n   is the same vocabulary as `cta.action` below, so one switch serves both\n   doors:\n\n   | `intent` | open | first call |\n   |---|---|---|\n   | `start` | question 1 | `POST .../quiz/attempts` |\n   | `retake` | question 1 | `POST .../quiz/attempts` |\n   | `resume` | resume at position | `POST .../quiz/attempts` → `resumed: true` |\n   | `view_results` | the score | `GET /quiz_attempts/{attempt_id}/results` |\n\n   `attempt_id` is present exactly for `resume` and `view_results`, which\n   address an attempt directly — so those two skip this endpoint entirely.\n   `locked` never appears: a blocked card renders prose, not a link.\n5. **Follow `cta.action` as given, including for an expired attempt.** When\n   `open_attempt.expired` is true the CTA is `view_results`, and\n   `GET /quiz_attempts/{id}/results` serves it: the attempt is graded from\n   its saved answers on the way in and the normal results payload comes\n   back with `attempt.state: \"timed_out\"` and `attempt.timed_out: true`. No\n   client-side rule and no probe `POST` first. (Posting first still works\n   and is what a `resume` CTA does — it replies 409 `attempt_timed_out`\n   carrying `details.attempt_id` — so a client already written that way\n   needs no change.)\n6. Treat `intent` as a **routing hint, not authority.** It is baked in when\n   the card renders, so a card left open long enough can say `resume` for a\n   clock that has since run out. Open the screen it names, then let the API's\n   answer win — start replies 409 `attempt_timed_out` and you route to\n   results instead. Same for \"may attempt\": don't carry it over from the\n   webview, the server refuses on its own with 403 `attempt_blocked`.\n7. Run the attempt, then **reload the WebView when your screen closes** —\n   passing writes the lesson completion and moves course progress, so the\n   card underneath is stale until you do.\n\nNot intercepting is supported, not broken: `/quiz/launch` is a real route\nthat starts or resumes the attempt and lands the learner in the responsive\nweb player. It is a GET precisely so it *can* be intercepted — a POST form\nis not reliably visible to Android's `shouldOverrideUrlLoading`.\n\n**Address the quiz by its LESSON, not by the quiz id.** A course version\ncan carry several quiz lessons, so the lesson id is what says which\nassessment is meant — and resolving through the lesson keeps a learner on a\nre-versioned course pinned to the copy they are actually taking.\n\n**Render `cta` rather than deriving one.** It resolves the same ladder the\nweb launch card uses, so three clients cannot each invent their own:\n\n| `cta.action`   | meaning                                                        |\n|----------------|----------------------------------------------------------------|\n| `start`        | nothing attempted yet — POST an attempt                         |\n| `resume`       | an attempt is open; `attempt_id` is it                          |\n| `retake`       | graded, and another attempt is allowed                          |\n| `view_results` | graded, and no further attempt is allowed (`reason` says why)   |\n| `locked`       | nothing graded and no attempt allowed (`reason` says why)       |\n\n`attempts.remaining` is **null, not 0**, when the quiz has no attempt\nlimit — an unlimited pool has no remainder, and 0 reads as exhausted.\n\n`outcome` is the attempt the learner's Pass/Fail VERDICT came from, which\nis not necessarily the most recent one: on a `highest` score policy a\nfailing retake leaves the verdict on the earlier pass. Do not infer the\nstanding result from the newest attempt.\n\n`outcome.effective_score` is the score that COUNTS toward completion and\nthe transcript, under the quiz's score policy (highest / latest / average /\nfirst). It can differ from `outcome.score_percentage`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 37
            }
          },
          {
            "name": "lesson_id",
            "in": "path",
            "required": true,
            "description": "The QUIZ lesson's id. A non-quiz lesson 404s.",
            "schema": {
              "type": "integer",
              "example": 412
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Quiz card retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "subject": {
                      "$ref": "#/components/schemas/TrainingSubjectBlock"
                    },
                    "enrollment": {
                      "$ref": "#/components/schemas/TrainingQuizEnrollmentState"
                    },
                    "card": {
                      "$ref": "#/components/schemas/TrainingQuizCard"
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (`access_denied`)."
          },
          "404": {
            "description": "`not_enrolled` — the caller holds no enrollment for this course, so the assessment is not yet something they can see. Also `not_found` for a course outside the caller's business, an unpublished or archived course, a lesson that is not in the learner's pinned version, or a lesson that carries no quiz. All four answer alike, so none reveals the others."
          }
        }
      }
    },
    "/training/courses/{course_id}/lessons/{lesson_id}/quiz/attempts": {
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Start or resume a quiz attempt",
        "description": "Returns the learner's live attempt, creating one only if there isn't\nalready one open — and the full question set with it, so the player needs\nno second request to begin.\n\n**Safe to retry.** A double tap, or a client retrying a request that timed\nout, cannot mint a second attempt or burn a slot out of the attempt pool:\nan open attempt is RESUMED. `resumed` tells you which happened, so the UI\ncan say \"resuming\" instead of implying a fresh clock. A resumed attempt\nkeeps the edition, the question order and the answers it already had.\n\n**200, not 201**, precisely because the common case is a resume — the\nstatus code would be a worse signal than `resumed`, which is unambiguous.\n\nRequires `write:training`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 37
            }
          },
          {
            "name": "lesson_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 412
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Attempt started or resumed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "resumed": {
                      "type": "boolean",
                      "description": "True when an attempt was already open and this returned it rather than creating one. Its clock has been running since `attempt.started_at`.",
                      "example": false
                    },
                    "attempt": {
                      "$ref": "#/components/schemas/TrainingQuizAttempt"
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible (`access_denied`); the token lacks `write:training` (`insufficient_permissions`); or `attempt_blocked` — a well-formed request the learner may not make. `error.message` is the specific gate, written for a learner: the attempt pool is used up, they have already passed and the quiz only allows retakes after a fail, or a cooldown is still running. 403 rather than 422 because nothing about the request is malformed."
          },
          "404": {
            "description": "As for the card — `not_enrolled` or `not_found`."
          },
          "409": {
            "description": "`attempt_timed_out` — the learner had an attempt still open whose clock had already run out. **This request graded it** from the answers it had saved, so there is nothing to resume. `error.details.attempt_id` names it: send the learner to that attempt's results, then let them choose Retake from there (which the results payload gates for you).\n\nYou can see this coming on the card — `open_attempt.expired` is true — and a client that checks it can label its button \"View result\" instead of \"Resume\" and skip this round trip. Do not treat it as an error to retry; retrying returns the same 409 until the learner starts a new attempt."
          },
          "422": {
            "description": "`no_questions` — the quiz has been published with nothing in it, so no edition can be sealed and there is nothing to take. `error.message` says so in words meant for the learner; show it rather than a generic error, and treat it as an admin problem, not a transient one worth retrying."
          }
        }
      }
    },
    "/training/quiz_attempts/{id}": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "The live quiz player",
        "description": "The open attempt and every question to render, in the order frozen onto the\nattempt when it started.\n\n**The whole set comes at once**, not one question per request. The\nassessment screen pages between questions with no loading state, answers\nautosave in the background, and the set is already frozen — so per-question\nfetching would put a network round trip inside the only interaction the\nscreen has, and could not surface a newer question anyway.\n\nRead the answer-stripping contract at the top of this section before\nrendering `questions`: nothing here identifies a correct answer, and for\n`ranking`, `matching_*` and `hotspot` that has consequences for how you\ndisplay and post each one.\n\n`questions[].answer` echoes what the learner has already entered, in the\nSAME form you post it — so a resumed attempt rehydrates. For `matching_*`\nthat echo is in tokens, not in the ids the server stores internally.\n\nThis endpoint refuses a finished attempt (409) rather than handing back a\nquiz the learner can no longer answer; the results endpoint is its\ncounterpart, and each refuses the other's state.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 9014
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Attempt retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "subject": {
                      "$ref": "#/components/schemas/TrainingSubjectBlock"
                    },
                    "lesson_id": {
                      "type": "integer",
                      "nullable": true,
                      "example": 412
                    },
                    "attempt": {
                      "$ref": "#/components/schemas/TrainingQuizAttempt"
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (`access_denied`)."
          },
          "404": {
            "description": "`not_found` — no such attempt for this caller."
          },
          "409": {
            "description": "The attempt is over, so there is no player to render. Two codes, both carrying `details.attempt_id` (send the learner to its results) and `details.block_reason` when a further attempt is refused:\n\n`attempt_finished` — it was already submitted.\n\n`time_expired` — the clock had run out. **This request finalized it**, grading whatever was last autosaved, so the attempt is now `timed_out`."
          }
        }
      }
    },
    "/training/quiz_attempts/{id}/answers": {
      "patch": {
        "tags": [
          "Training"
        ],
        "summary": "Autosave answers on an open attempt",
        "description": "Persist answers-in-progress. Fire this on every question change (debounce\nit when the whole quiz is on one page) so backgrounding the app, losing\nsignal or a crash never loses answered work — and so a timed attempt that\nexpires is graded from something real.\n\nDeliberately does **not** score, change the attempt's state, or return the\nquestions. Submit remains the authority.\n\nSend the full `answers` object each time, not a delta: it REPLACES what was\nstored. `position` records where the learner was, so a resume reopens on\nthat question.\n\nReturns the saved tally (`answered_count` / `question_count`) so a client\ncan drive an \"N of 7 answered\" line — including the one on the exit\nconfirmation — without counting locally. \"Answered\" is judged per type: a\n`true_false` of `false` counts, a `matching_*` needs at least one filled\npair, and a `ranking` always counts because it always has an order.\n\nRequires `write:training`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The attempt id.",
            "schema": {
              "type": "integer",
              "example": 9014
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "position": {
                    "type": "integer",
                    "description": "1-based question index the learner is on.",
                    "example": 3
                  },
                  "answers": {
                    "$ref": "#/components/schemas/TrainingQuizAnswers"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Answers saved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "saved": {
                      "type": "boolean",
                      "example": true
                    },
                    "attempt": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 9014
                        },
                        "current_position": {
                          "type": "integer",
                          "nullable": true,
                          "example": 3
                        },
                        "answered_count": {
                          "type": "integer",
                          "example": 2
                        },
                        "question_count": {
                          "type": "integer",
                          "example": 7
                        },
                        "time_remaining_seconds": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Null on an untimed quiz.",
                          "example": 742
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible, or the token lacks `write:training`."
          },
          "404": {
            "description": "`not_found` — no such attempt FOR THIS CALLER, or its course is no longer visible. Another learner's attempt answers identically."
          },
          "409": {
            "description": "`attempt_closed` — the attempt has already been submitted or timed out, so nothing was saved and the stored answers are untouched. Stop retrying and take the learner to the results."
          }
        }
      }
    },
    "/training/quiz_attempts/offline": {
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Replay a quiz attempt taken offline (untimed quizzes only)",
        "description": "One shot: create AND grade an attempt from answers the learner gave with no\nconnection, and return the same result payload `submit` does — score, review\nand the learner's new standing in the course. A pass completes the quiz's\nlesson exactly as the online path does.\n\n**Untimed quizzes only.** A quiz with `time_limit_minutes` is refused with\n`offline_not_permitted_timed` and nothing is created: the clock runs on the\nserver from `started_at` and cannot be honest about minutes that passed on a\ndevice. Read `quiz.offline_allowed` on the quiz card, or\n`offline.quiz` on the lesson content payload, to know in advance.\n\n**Idempotent on `client_attempt_id`.** Generate a UUID on the device when the\nlearner starts, send it with every flush: a retried request returns the\noriginal result with `outcome: replayed` and `replayed: true`, and spends no\nsecond attempt. Nothing about the payload is re-graded on a replay.\n\n**Entitlement is re-checked at replay.** A learner who is out of attempts,\nalready passed under `only_after_fail`, or inside a cooldown gets `403\nattempt_blocked` with the same prose the card shows — and nothing is created,\nso the client shows the refusal and discards its local draft.\n\n**Key `answers` by the question ids from `offline.quiz.questions`** on the\nlesson content payload (the quiz's sealed edition). Shapes per question type\nare the ones `PATCH /quiz_attempts/{id}/answers` accepts — matching via the\nopaque tokens, ranking as an ordered id list, hotspot as `[{x, y}]`. Mandatory\nquestions left unanswered are refused with `unanswered_mandatory` and nothing\nis created.\n\n`started_at` / `finished_at` are the device's timestamps and become the\nattempt's clock (bounded to the last 30 days, never the future).\n\nRequires `write:training`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "lesson_id",
                  "client_attempt_id",
                  "answers"
                ],
                "properties": {
                  "lesson_id": {
                    "type": "integer",
                    "example": 1463
                  },
                  "course_id": {
                    "type": "integer",
                    "description": "Optional — derived from the lesson when absent.",
                    "example": 572
                  },
                  "client_attempt_id": {
                    "type": "string",
                    "maxLength": 64,
                    "description": "A UUID minted on the device when the learner started.",
                    "example": "2f4d1c0a-6f4e-4d2e-9b1a-3c9f0e7d5a11"
                  },
                  "started_at": {
                    "type": "string",
                    "format": "date-time",
                    "example": "2026-09-07T09:12:00Z"
                  },
                  "finished_at": {
                    "type": "string",
                    "format": "date-time",
                    "example": "2026-09-07T09:26:40Z"
                  },
                  "answers": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "Keyed by question id, in the per-type shapes the answers endpoint accepts.",
                    "example": {
                      "9101": "a",
                      "9102": [
                        "c",
                        "a",
                        "b"
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The attempt was created and graded (or, on a replay, the original result)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "outcome": {
                      "type": "string",
                      "enum": [
                        "submitted",
                        "replayed"
                      ]
                    },
                    "replayed": {
                      "type": "boolean",
                      "description": "True when this client_attempt_id had already been flushed — the original result, nothing consumed."
                    },
                    "result": {
                      "$ref": "#/components/schemas/TrainingQuizResult"
                    },
                    "course": {
                      "$ref": "#/components/schemas/TrainingQuizCourseProgress"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`attempt_blocked` — attempts used up, already passed, or cooling down; nothing created"
          },
          "404": {
            "description": "Not enrolled, or no quiz on this lesson"
          },
          "422": {
            "description": "`offline_not_permitted_timed` (a timed quiz), `unanswered_mandatory`, `invalid_client_attempt_id` or `no_questions`; nothing created"
          }
        }
      }
    },
    "/training/quiz_attempts/{id}/submit": {
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Submit a quiz attempt for grading",
        "description": "Grade and close the attempt, and return the full result — score, review and\nthe learner's new standing in the course — so the results screen needs no\nfollow-up request.\n\nKey the `answers` object by the question ids THIS attempt's player\nreturned. See the answer-stripping and edition notes at the top of this\nsection; ids from anywhere else will report every question unanswered.\n\n**Passing completes the quiz's lesson**, which moves course progress and,\nwhen this was the last outstanding requirement, completes the enrollment.\nThe `course` block reports that new state, which is what tells a client\nwhether to show a course-completion screen. `course.certificate.status` is\n`pending` right after such a completion because issuing it is asynchronous —\npoll `GET /training/my_records/certificates`; do not read a missing id as\n\"no certificate\".\n\n**`outcome` has two values.** `submitted` is the normal path. `timed_out`\nmeans the clock had already run out: the attempt was graded from the last\nautosave and the payload just sent was ignored. Show \"time's up\" rather\nthan presenting it as a normal submission.\n\nRequires `write:training`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 9014
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "answers": {
                    "$ref": "#/components/schemas/TrainingQuizAnswers"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Attempt graded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "outcome": {
                      "type": "string",
                      "enum": [
                        "submitted",
                        "timed_out"
                      ]
                    },
                    "result": {
                      "$ref": "#/components/schemas/TrainingQuizResult"
                    },
                    "course": {
                      "$ref": "#/components/schemas/TrainingQuizCourseState"
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible, or the token lacks `write:training`."
          },
          "404": {
            "description": "`not_found` — no such attempt for this caller."
          },
          "409": {
            "description": "`already_submitted` — a double submit, a stale screen, or a retry after the attempt was graded. **The answers in this request were NOT recorded** and the earlier grade stands. Say so rather than quietly showing the old results."
          },
          "422": {
            "description": "`unanswered_mandatory` — one or more required questions are blank. Nothing was graded and the attempt stays open. The submitted answers were kept as a draft, so the learner loses nothing. `error.details.unanswered_question_ids` names the questions and `error.details.unanswered_count` how many, so a client can jump the learner straight to the first one."
          }
        }
      }
    },
    "/training/quiz_attempts/{id}/results": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "Results of a graded attempt",
        "description": "The post-submit review: the score banner, the per-question outcome list,\nevery graded attempt for the switcher, and whether a retake is still on\noffer.\n\nAttempt-addressed, so a learner can look back at any of their own graded\nattempts — which is what the `result.attempts.graded` list is for. The\nentry with `current: true` is the one being shown.\n\n**An expired attempt is graded on the way in.** A timed attempt left open\npast its limit is still `in_progress` with a null `submitted_at` until\nsomething finalizes it, and this endpoint does: it grades the answers that\nwere autosaved, writes the quiz-lesson completion when the score clears the\nbar, and renders the normal payload with `attempt.state: \"timed_out\"` and\n`attempt.timed_out: true`. So the `view_results` CTA the quiz card emits for\nsuch an attempt can be followed directly. Repeating the call is safe — the\nattempt is already terminal, so nothing is re-graded or re-stamped.\n\n**`review` can be null.** Answer reveal is a per-quiz setting frozen with\nthe attempt's edition, so an admin turning it off cannot retroactively blank\na review a learner has already seen, and turning it on cannot reveal answers\nfor an attempt taken under the old rule. When it is null,\n`review_hidden_reason` is `answers_not_revealed` — render that, not an empty\nlist, which reads as a bug.\n\n**The score's three counts are reported separately on purpose.** Written\n(`text`) answers are graded by a human and are excluded from BOTH sides of\nthe score, so `correct_count` / `auto_graded_count` / `pending_review_count`\nare given rather than a single \"N of M correct\" that would silently count a\nquestion nobody has read. Compose the sentence from figures that add up, and\nrender a `pending_review` row as awaiting review — never as incorrect.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 9014
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Results retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "subject": {
                      "$ref": "#/components/schemas/TrainingSubjectBlock"
                    },
                    "lesson_id": {
                      "type": "integer",
                      "nullable": true,
                      "example": 412
                    },
                    "result": {
                      "$ref": "#/components/schemas/TrainingQuizResult"
                    },
                    "course": {
                      "$ref": "#/components/schemas/TrainingQuizCourseState"
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (`access_denied`)."
          },
          "404": {
            "description": "`not_found` — no such attempt for this caller."
          },
          "409": {
            "description": "`attempt_not_submitted` — the attempt is still LIVE (its clock is running and it has no score yet). Send the learner to `GET /training/quiz_attempts/{id}` to go on answering. An open attempt whose time limit has EXPIRED is not refused: it is graded from its saved answers and returned as a normal 200 with `attempt.timed_out: true`."
          }
        }
      }
    },
    "/training/courses/{course_id}/lessons/{lesson_id}/scorm": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "SCORM card (courseware launch panel)",
        "description": "Everything the learner sees BEFORE launching a courseware module: what the\npackage is, whether it can be played at all, their status and score, where\nthey left off, and the ONE control to offer — plus the url that opens the\nreal player.\n\nThis is the payload behind the prototype's launch panel for a package\nlesson, and the native mirror of the two web launch cards. Re-read it every\ntime your runtime WebView closes: the player writes progress straight onto\nthe lesson completion, so the card underneath is stale until you do.\n\n**THIS ENDPOINT DOES NOT HAND YOU CONTENT TO RENDER, AND CANNOT.** A quiz\nis JSON, so a native screen can take it. A SCORM / xAPI / cmi5 / AICC\npackage is a folder of third-party HTML and JavaScript that talks to a\nruntime API on this origin (SCORM API calls, the LRS, AICC HACP) — there is\nno payload that would let an app play it. So:\n\n1. Draw the card from this payload.\n2. When the learner taps the primary control, open `cta.web_view_url` in a\n   WebView. It is the chrome-less full-frame player (`?embed=1` strips the\n   header, tab bar and prev/next so your own title bar is the only one).\n   **That request is what starts the attempt** — it mints the courseware\n   registration and a single-use per-launch session — so do not prefetch\n   it, and do not open it to \"check\" the card.\n3. Draw your own exit affordance. The player also carries a \"Return to\n   course\" control which navigates the WebView to the lesson page\n   (`/m/apps/training/courses/{course_id}/lessons/{lesson_id}?embed=1`) —\n   match that path to close your screen instead.\n4. Reload the card when your screen closes.\n\n**`cta.web_view_url` is null whenever there is nothing to open**, so a\nclient that renders the button only when it is present can never offer a\ncontrol that lands on a refusal. `cta.reason` says why, and `cta.message`\nis prose written for a learner — show it.\n\n| `cta.action`  | meaning                                                        |\n|---------------|----------------------------------------------------------------|\n| `launch`      | never opened — open `web_view_url`                              |\n| `resume`      | opened before and unfinished; `resume` says where they were     |\n| `review`      | the module is complete — it re-opens READ-ONLY (see below)      |\n| `processing`  | the package is still importing — no launch yet, check back      |\n| `unavailable` | it cannot be played (`reason` says why)                         |\n\n**A COMPLETED MODULE IS REVIEW-ONLY, and that is a rule about the record,\nnot a label.** The moment the lesson completes, its score and training\ntime are frozen: the runtime keeps working (it still bookmarks, still\nshows its own score inside the player) but nothing it reports afterwards\ncan move `completion.score` or `completion.time_spent_seconds`. So do not\npresent `review` as a retake or offer a \"try for a better score\" — the\nlearner cannot earn one, and a client that implies otherwise is lying to\nthem. A genuine second attempt is a course RETAKE, which mints a new\nenrollment attempt and a fresh record.\n\n`cta.secondary` is the \"Start over\" that discards the bookmark\n(`POST .../scorm/restart`). It appears **only while the lesson is\nunfinished** and there is something to discard — never on a `review`\ncard, for the reason above. Both web cards and the player's own menu\nfollow the identical rule, so a learner never sees it in one place and\nnot another.\n\n**Render `package.launchable`, never `package.status`.** `status` is the\nLESSON's import state; `launchable` is whether a launch can actually\nproduce a player (the package imported AND its files landed, or an AICC\nunit that runs on the provider). They disagree in real data — a tenant\ncarrying lessons from before the native engine has them stamped `ready`\nwith no package at all — and `launchable` is the gate the launch itself\napplies.\n\n**There is no attempt pool.** Courseware has no attempt limit anywhere in\nthe model: one registration exists per lesson attempt and each launch opens\na SESSION on it, which is what `resume.launches` counts. Do not render\n\"attempt 1 of 3\" for a module.\n\n**Courseware is online-only** (`offline.supported: false`). The runtime is\nserver-side, so there is nothing a client can store and play on a train —\nunlike a text, video or document lesson, which\n`GET .../lessons/{lesson_id}/content` packages for offline use.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 81
            }
          },
          {
            "name": "lesson_id",
            "in": "path",
            "required": true,
            "description": "The SCORM lesson's id. A lesson of any other content type 404s — a course version can carry several courseware lessons, so the lesson id is what says which module is meant.",
            "schema": {
              "type": "integer",
              "example": 289
            }
          }
        ],
        "responses": {
          "200": {
            "description": "SCORM card retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "subject": {
                      "$ref": "#/components/schemas/TrainingSubjectBlock"
                    },
                    "enrollment": {
                      "$ref": "#/components/schemas/TrainingQuizEnrollmentState"
                    },
                    "card": {
                      "$ref": "#/components/schemas/TrainingScormCard"
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (`access_denied`)."
          },
          "404": {
            "description": "`not_enrolled` — the caller holds no enrollment for this course, so its modules are not yet something they can open. Also `not_found` for a course outside the caller's business, an unpublished or archived course, a lesson that is not in the learner's pinned version, or a lesson that is not a SCORM lesson. All of them answer alike, so none reveals the others."
          }
        }
      }
    },
    "/training/courses/{course_id}/lessons/{lesson_id}/scorm/restart": {
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Start a courseware module over",
        "description": "Drops this attempt's courseware registration — the bookmark, the suspend\ndata, the runtime's score and the session history — so the next launch\nplays from the beginning. The card's `cta.secondary` is what offers it,\nand the desktop card's \"Start over\" button is the same operation through\nthe same service.\n\n**ONLY WHILE THE LESSON IS UNFINISHED.** Once it is complete this answers\n403 `lesson_completed` and changes nothing. That is not a permission quirk\n— a completed lesson's score and training time are frozen, so a reset\nthere would throw away the learner's place and then record nothing from\nthe replay. A finished module is review-only; a real do-over is a course\nretake (new attempt, new record) or an admin reset from Courseware\nactivity, which is the one door allowed to clear a finished learner.\n\n**A recorded completion STAYS recorded.** Even on an unfinished lesson\nthis resets where the learner is, never what has been recorded: the\ncompletion row, the course progress it drives and any certificate it\nearned are untouched. Same contract in both web doors.\n\n**Idempotent.** With nothing to reset it removes nothing and still answers\n200 — `reset` says which happened, so a client retrying a dropped request\ncannot do damage and does not have to guess.\n\nReturns the refreshed CARD, so redraw from this response rather than firing\na second GET to discover that Resume has become Launch.\n\nRequires `write:training`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 81
            }
          },
          {
            "name": "lesson_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 289
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Progress reset (or nothing to reset)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "reset": {
                      "type": "boolean",
                      "description": "True when a registration was actually dropped. False means there was nothing to reset — the module had never been launched, or a previous call already did it.",
                      "example": true
                    },
                    "subject": {
                      "$ref": "#/components/schemas/TrainingSubjectBlock"
                    },
                    "enrollment": {
                      "$ref": "#/components/schemas/TrainingQuizEnrollmentState"
                    },
                    "card": {
                      "$ref": "#/components/schemas/TrainingScormCard"
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible (`access_denied`); the token lacks `write:training` (`insufficient_permissions`); `lesson_completed` — the module is already complete and is review-only, so there is nothing to start over (the card shows no `cta.secondary` in this state); or `enrollment_cancelled` — the attempt has been cancelled, so there is nothing to record a replay against."
          },
          "404": {
            "description": "As for the card — `not_enrolled` or `not_found`."
          },
          "409": {
            "description": "`package_processing` / `package_stalled` / `package_unavailable` — the module cannot be played, so its resume state is left alone rather than wiped for something the learner cannot reopen. The same code the card's `cta.reason` carries; redraw the card rather than retrying."
          }
        }
      }
    },
    "/training/courses/{course_id}/reviews": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "Course ratings & reviews",
        "description": "The FULL Ratings & Reviews list for a course — the \"See all\" sheet behind\nthe summary block on the course detail. Paginated and sortable.\n\nThe detail endpoint (`GET /training/courses/{id}`) already inlines the\nnewest 5 reviews plus the same `rating` summary; this is what the client\nopens for the rest. The summary is repeated because the sheet renders its\nown histogram header.\n\nAPPROVED reviews only — the same filter the web block applies. Ordering:\n`recent` (newest first, the default) and `lowest` (lowest rating first,\nfor a learner looking for the caveats before starting).\n\nThere is no `helpful` sort and no `helpful_count` on a row. The Helpful\naffordance is admin-only on the web — its button and count live on the\nadmin reviews index, which no learner surface links to — so a\nlearner-facing list neither displays that number nor orders by it.\n`sort=helpful` is treated as unrecognised and falls back to `recent`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 37
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Unrecognised values fall back to `recent`.",
            "schema": {
              "type": "string",
              "enum": [
                "recent",
                "lowest"
              ],
              "default": "recent"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Clamped to 1..50; anything unparseable falls back to the default.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Reviews retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/TrainingReviewsPage"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "unread_notification_count": {
                          "$ref": "#/components/schemas/UnreadNotificationCount"
                        },
                        "_meta": {
                          "$ref": "#/components/schemas/ResponseMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (error code `access_denied`)."
          },
          "404": {
            "description": "No such course visible to this caller (error code `not_found`)."
          }
        }
      },
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Rate a course",
        "description": "**Requires the `write:training` scope.** Every first-party token carries\nit (login, refresh and SSO all mint the read/write pair together); a token\nwithout it gets 403 `insufficient_permissions`. Training READS are not\nscope-gated.\n\nPost the caller's review of a course — the native mirror of the learner's\ninline \"Rate this course\" modal on the web About tab.\n\n**Eligibility** is the same rule the GET reports as `can_review`: the\ncaller must be ENROLLED and must not have reviewed this course already.\nA client that honours `can_review` never hits the two 422s below; they\nexist for a stale screen, and they carry DIFFERENT codes because the\nclient's next move differs:\n\n* `already_reviewed` — offer *Edit your review* instead. (A review still\n  awaiting moderation counts, so this fires whenever `my_review` is set.)\n* `not_enrolled` — offer *Enroll* instead.\n\n**One review per learner per course**, enforced by both a model\nvalidation and a unique index. There is no POST-to-update: re-posting is\n`already_reviewed`.\n\n**Verified purchase** is stamped server-side when the caller bought the\ncourse, exactly as on the web — clients neither send nor control it.\n\n**Moderation**: reviews are approved on creation (the column default), so\nthe new review is live and appears in the very next GET of this list. The\nresponse returns the recomputed `rating` histogram, so a client refreshes\nits summary block without a second call.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 37
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TrainingReviewCreateRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Review posted",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/TrainingReviewWriteResult"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "unread_notification_count": {
                          "$ref": "#/components/schemas/UnreadNotificationCount"
                        },
                        "_meta": {
                          "$ref": "#/components/schemas/ResponseMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (error code `access_denied`)."
          },
          "404": {
            "description": "No such course visible to this caller (error code `not_found`)."
          },
          "422": {
            "description": "Not eligible (`already_reviewed`, `not_enrolled`) — both in the\nstandard `{ error: { code, message } }` envelope — **or** the body\nfailed validation (a missing/out-of-range `rating`, an over-long\n`title`/`content`), which answers with the platform's per-field\n`{ errors: [{ field, message, code }] }` shape instead.\n"
          }
        }
      }
    },
    "/training/learning_paths/{learning_path_id}/reviews": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "Learning-path ratings & reviews",
        "description": "The FULL Ratings & Reviews list for a learning path. Identical payload and\nparameters to the course variant above (`subject.type` is `path`) — reviews\nare polymorphic, so one controller serves both.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "learning_path_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 1
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Unrecognised values fall back to `recent`.",
            "schema": {
              "type": "string",
              "enum": [
                "recent",
                "lowest"
              ],
              "default": "recent"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Clamped to 1..50; anything unparseable falls back to the default.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Reviews retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/TrainingReviewsPage"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "unread_notification_count": {
                          "$ref": "#/components/schemas/UnreadNotificationCount"
                        },
                        "_meta": {
                          "$ref": "#/components/schemas/ResponseMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (error code `access_denied`)."
          },
          "404": {
            "description": "No such learning path visible to this caller (error code `not_found`)."
          }
        }
      },
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Rate a learning path",
        "description": "**Requires the `write:training` scope.** Every first-party token carries\nit (login, refresh and SSO all mint the read/write pair together); a token\nwithout it gets 403 `insufficient_permissions`. Training READS are not\nscope-gated.\n\nPost the caller's review of a learning path. Identical body, payload and\nerror codes to the course variant above — reviews are polymorphic, so one\ncontroller serves both.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "learning_path_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 1
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TrainingReviewCreateRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Review posted",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/TrainingReviewWriteResult"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "unread_notification_count": {
                          "$ref": "#/components/schemas/UnreadNotificationCount"
                        },
                        "_meta": {
                          "$ref": "#/components/schemas/ResponseMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (error code `access_denied`)."
          },
          "404": {
            "description": "No such learning path visible to this caller (error code `not_found`)."
          },
          "422": {
            "description": "Not eligible (`already_reviewed`, `not_enrolled`), or the body failed\nvalidation — see the course variant above for both envelopes.\n"
          }
        }
      }
    },
    "/training/courses/{course_id}/questions": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "Course Q&A",
        "description": "The Course Q&A screen — question threads with their answers, the native\nmirror of the Q&A tab on the web course page.\n\nEach thread carries its answers ordered best-answer-first then\nmost-upvoted (the web's exact sort), plus `best_answer` as a pointer into\nthat array for clients showing a single answer.\n\n`filter` is the tab's three pills: `all` (newest first, the default),\n`unanswered`, and `top` (most upvoted). `counts` is filter-INDEPENDENT so\none request badges all three.\n\nThe detail endpoint's `qa: { questions_count, unanswered_count }` block is\nthe tab badge; this is the tab's contents.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 37
            }
          },
          {
            "name": "filter",
            "in": "query",
            "required": false,
            "description": "Unrecognised values fall back to `all`.",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "unanswered",
                "top"
              ],
              "default": "all"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Clamped to 1..50; anything unparseable falls back to the default.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Q&A retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/TrainingQuestionsPage"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "unread_notification_count": {
                          "$ref": "#/components/schemas/UnreadNotificationCount"
                        },
                        "_meta": {
                          "$ref": "#/components/schemas/ResponseMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (error code `access_denied`)."
          },
          "404": {
            "description": "No such course visible to this caller (error code `not_found`)."
          }
        }
      },
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Ask a question, or reply to one",
        "description": "**Requires the `write:training` scope.** Every first-party token carries\nit (login, refresh and SSO all mint the read/write pair together); a token\nwithout it gets 403 `insufficient_permissions`. Training READS are not\nscope-gated.\n\n**ONE endpoint for both halves of a Q&A thread**, because they are one\ncomposer to the client:\n\n* omit `question_id` → ASK a new question on this course\n* pass  `question_id` → REPLY (post an answer) to that question\n\nMirrors the web's two actions (ask + answer) on the same screen.\n\n**Threading is one level**, structurally: an answer has no parent answer,\nso there is no reply-to-a-reply case. `question_id` is resolved through\nTHIS course's own thread — an id belonging to another course, another\nlearning path or another tenant is 422 `question_not_found`, never a\nmisfiled answer.\n\n**No per-owner permission**: any Training user in the business may ask and\nanswer (`permissions.can_ask` / `can_answer` are true for anyone holding a\n200 on the GET). Only marking a best answer is gated — see\n`POST /training/qa/answers/{id}/mark_best`.\n\n**No notifications** are sent, matching the web: an instructor finds\nunanswered questions from the tab's own `counts.unanswered` badge.\n\nThe response is the WHOLE updated thread, not just the row written, so one\nresponse re-renders the card — posting an answer also flips the question's\n`status` to `answered` and re-sorts `answers`. `answer_id` names the row\njust created (null when a question was asked) so a client can highlight it\nwithout diffing.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 37
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TrainingQaPostRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Question asked, or answer posted",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/TrainingQaThread"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "unread_notification_count": {
                          "$ref": "#/components/schemas/UnreadNotificationCount"
                        },
                        "_meta": {
                          "$ref": "#/components/schemas/ResponseMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (error code `access_denied`)."
          },
          "404": {
            "description": "No such course visible to this caller (error code `not_found`)."
          },
          "422": {
            "description": "`body_required` (nothing but whitespace was sent) or\n`question_not_found` (`question_id` isn't a question on this course),\nboth in the standard `{ error: { code, message } }` envelope — or a\nmodel validation failure (body over its length cap) in the per-field\n`{ errors: [...] }` shape.\n"
          }
        }
      }
    },
    "/training/learning_paths/{learning_path_id}/questions": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "Learning-path Q&A",
        "description": "The Q&A thread list for a learning path. Identical payload and parameters\nto the course variant above (`subject.type` is `path`) — Q&A is\npolymorphic, so one controller serves both.\n\nOne difference in the DATA, not the shape: `author.instructor` is always\nfalse here. The Instructor role is per-COURSE; a learning path has no\nequivalent, which is why the web badge never appears on a path thread\neither.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "learning_path_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 1
            }
          },
          {
            "name": "filter",
            "in": "query",
            "required": false,
            "description": "Unrecognised values fall back to `all`.",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "unanswered",
                "top"
              ],
              "default": "all"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Clamped to 1..50; anything unparseable falls back to the default.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Q&A retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/TrainingQuestionsPage"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "unread_notification_count": {
                          "$ref": "#/components/schemas/UnreadNotificationCount"
                        },
                        "_meta": {
                          "$ref": "#/components/schemas/ResponseMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (error code `access_denied`)."
          },
          "404": {
            "description": "No such learning path visible to this caller (error code `not_found`)."
          }
        }
      },
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Ask a question on a learning path, or reply to one",
        "description": "**Requires the `write:training` scope.** Every first-party token carries\nit (login, refresh and SSO all mint the read/write pair together); a token\nwithout it gets 403 `insufficient_permissions`. Training READS are not\nscope-gated.\n\nIdentical body, payload and error codes to the course variant above — Q&A\nis polymorphic, so one controller serves both. Omit `question_id` to ask,\npass it to reply.\n\nOne difference in the DATA, not the shape: `permissions.can_mark_best` is\ntrue for a Training admin or the PATH'S CREATOR here, because the\nInstructor role is per-COURSE and a path has no equivalent (the same\nbranch the web tab takes).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "learning_path_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 1
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TrainingQaPostRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Question asked, or answer posted",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/TrainingQaThread"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "unread_notification_count": {
                          "$ref": "#/components/schemas/UnreadNotificationCount"
                        },
                        "_meta": {
                          "$ref": "#/components/schemas/ResponseMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (error code `access_denied`)."
          },
          "404": {
            "description": "No such learning path visible to this caller (error code `not_found`)."
          },
          "422": {
            "description": "`body_required` or `question_not_found`, or a model validation\nfailure — see the course variant above for both envelopes.\n"
          }
        }
      }
    },
    "/training/qa/{votable_type}/{id}/vote": {
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Toggle an upvote on a question or an answer",
        "description": "**Requires the `write:training` scope.** Every first-party token carries\nit (login, refresh and SSO all mint the read/write pair together); a token\nwithout it gets 403 `insufficient_permissions`. Training READS are not\nscope-gated.\n\n**TOGGLE** the caller's upvote (\"helpful\") on a question OR an answer —\nthe native mirror of the web's two upvote buttons, which are the same\ntoggle on the same polymorphic vote record.\n\nONE endpoint, with `votable_type` selecting the target, for the same\nreason one controller serves both owner types on the read side: the vote\nis polymorphic, and the web's two actions differ only in which row they\nload.\n\n**A toggle, not a POST/DELETE pair** (unlike the Ideas vote API): a\nTraining upvote un-votes on a second press on every surface, and the tap a\nclient is mirroring doesn't know which direction it is going. So this is\n**NOT idempotent** — two calls return to the starting state — and the\nresponse always reports the RESULTING state read back from the database.\nPatch your cached row from `votes_count` / `my_vote`; never predict them.\n\nNo per-owner permission — upvoting is open to any Training user in the\nbusiness, exactly as on the web.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "votable_type",
            "in": "path",
            "required": true,
            "description": "Which kind of row to toggle. Anything other than these two words does not match the route and 404s at the router.",
            "schema": {
              "type": "string",
              "enum": [
                "questions",
                "answers"
              ]
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The question's or answer's own id (not the course's).",
            "schema": {
              "type": "integer",
              "example": 30
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Upvote toggled; the resulting state is returned.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "type": "object",
                      "required": [
                        "votable"
                      ],
                      "properties": {
                        "votable": {
                          "$ref": "#/components/schemas/TrainingQaVoteState"
                        }
                      }
                    },
                    {
                      "type": "object",
                      "properties": {
                        "unread_notification_count": {
                          "$ref": "#/components/schemas/UnreadNotificationCount"
                        },
                        "_meta": {
                          "$ref": "#/components/schemas/ResponseMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (error code `access_denied`)."
          },
          "404": {
            "description": "No such question/answer in the caller's business, or its course / learning path is no longer visible to the caller (error code `not_found` for both — neither answer reveals the other)."
          }
        }
      }
    },
    "/training/qa/answers/{id}/mark_best": {
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Mark an answer as the best answer",
        "description": "**Requires the `write:training` scope.** Every first-party token carries\nit (login, refresh and SSO all mint the read/write pair together); a token\nwithout it gets 403 `insufficient_permissions`. Training READS are not\nscope-gated.\n\nMark one answer as its question's BEST answer — the native mirror of the\nweb Mark-best affordance. Clears whichever answer held the flag before and\nflips the question to `answered`, in one transaction.\n\n**The one Q&A action with a per-owner permission**: the owner's content\nmanager only — a Training admin, the COURSE's Instructor, or (for a\nlearning path, which has no per-path role) the path's creator. Learners\nmay ask, answer and upvote; deciding which answer is authoritative is\nmoderation.\n\nThe Q&A GET reports the same decision as `permissions.can_mark_best`, so\nrender the affordance from that flag rather than from the existence of\nthis endpoint — otherwise every learner sees a button that 403s on tap.\n\n**Not a toggle**: there is no un-mark on any surface. Re-marking the answer\nthat already holds the flag is a successful no-op; marking a DIFFERENT\nanswer moves the flag, which is how a mistake is corrected.\n\nAnswers with the updated thread — the same payload the Q&A POST returns —\nbecause marking a best answer re-sorts `answers` (best first) and can\nchange the question's `status`, so a narrower response would leave the\ncard wrong.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The answer's own id.",
            "schema": {
              "type": "integer",
              "example": 23
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Best answer set; the updated thread is returned.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/TrainingQaThread"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "unread_notification_count": {
                          "$ref": "#/components/schemas/UnreadNotificationCount"
                        },
                        "_meta": {
                          "$ref": "#/components/schemas/ResponseMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (`access_denied`), or the caller is not a content manager of the answer's course / learning path (`forbidden`)."
          },
          "404": {
            "description": "No such answer in the caller's business, or its course / learning path is no longer visible to the caller (error code `not_found`)."
          },
          "422": {
            "description": "`mark_best_failed` — two managers marked a best answer on the same question at the same moment and the unique index rejected this one. Retrying succeeds; re-offer the action."
          }
        }
      }
    },
    "/training/my_team": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "My Team dashboard (one tab per request)",
        "description": "The manager surface: the four stat tiles, the three pill counts, and **one\npage of the active tab**.\n\n**Fetch the tab you are showing.** `tab` selects which list comes back —\n`people` (the roster, the default), `overdue`, or `done` — and `page` /\n`per_page` paginate **that** list, with `meta` describing it. An\nunrecognised `tab` falls back to `people`, and `active_tab` echoes what was\nactually applied.\n\n**Named keys, one populated.** `members`, `overdue` and `completions`\nalways all appear; only the active tab's carries rows. That keeps the\nelement type of each key stable for a generated client, which a single\npolymorphic `rows` key would not. Read `active_tab` to know which to use.\n\n**`stats` and `pills` are TAB-INDEPENDENT**, so one request still badges\nall three pills and switching tabs needs no second count.\n\n**SCOPE: the role sets the default AND the ceiling.** An admin/HR\nadmin/Training app admin defaults to every active member of the business;\na people-manager sees their direct reports. `scope` reports what was\nactually applied:\n\n- `scope.key` — `all_employees` or `direct_reports`, the scope in force.\n- `scope.available` — what THIS caller may ask for: both values for an\n  admin, `[\"direct_reports\"]` alone for a manager, so a client renders the\n  segmented control only when there is a real choice.\n- `scope.admin_view` — whether these rows are the whole business. Derived\n  from the APPLIED scope, not from the role, so an admin who has narrowed\n  to their reports gets the \"Team\" noun rather than \"Employees\".\n\nPass `scope` to switch. An admin may narrow to `direct_reports`; a manager\nasking for `all_employees` is served their reports regardless — the role is\na ceiling, not a suggestion — and `scope.key` echoes what was applied.\nUnrecognised values fall back to the default, the same convention `tab`\nuses.\n\nThe drill-in (`/training/my_team/{id}`) deliberately IGNORES `scope`: it\nresolves through the caller's entitled scope, so narrowing the dashboard\nnever turns another employee's row into a 404.\n\n`scope.total` carries the true headcount beside the page — what a\n\"Showing 8 of 142\" caption needs. `stats.team_size` is that same total.\n`pills[].key` values are stable and match the `tab` values.\n\nThe `overdue` and `done` lists span the WHOLE team, not the roster page,\nbecause their pill counts the team — a list that disagreed with its own\nbadge would be worse than a slow one.\n\n> **Why one tab per request.** An earlier revision returned all three\n> lists every time and fed each of them the whole team's ids. On a\n> 41,260-person tenant that measured 8.1 s, 157 queries and 2.5 MB of SQL\n> text — eight statements carrying a 41,262-element `IN (...)` list — to\n> produce a 15 KB payload. Per-tab fetching plus subquery filtering brings\n> the same request to ~110 ms, and nothing on the path scales with\n> headcount.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "tab",
            "in": "query",
            "required": false,
            "description": "Which list to return. Matches the `pills[].key` values. Unrecognised values fall back to `people`.",
            "schema": {
              "type": "string",
              "enum": [
                "people",
                "overdue",
                "done"
              ],
              "default": "people"
            }
          },
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "description": "Which team to report on. `all_employees` is admin-only and is an admin's default; `direct_reports` is a manager's only option and their default. A manager passing `all_employees` is still served their direct reports (the role is a ceiling). Unrecognised values fall back to the caller's default. `scope.key` in the response echoes what was applied, and `scope.available` lists what this caller may ask for.",
            "schema": {
              "type": "string",
              "enum": [
                "direct_reports",
                "all_employees"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Page size for the ACTIVE tab. Clamped to 1..50; anything unparseable falls back to 20.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Dashboard retrieved",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/TrainingMyTeamDashboard"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "unread_notification_count": {
                          "$ref": "#/components/schemas/UnreadNotificationCount"
                        },
                        "_meta": {
                          "$ref": "#/components/schemas/ResponseMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (`access_denied`), or the caller is neither a people-manager nor a Training admin (`forbidden`)."
          }
        }
      }
    },
    "/training/my_team/{id}": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "One team member's training",
        "description": "A learner's drill-in: their four counts, everything outstanding, the\ncompleted history, and their issued certificates.\n\nResolved THROUGH the caller's own team scope, so a manager cannot address\nsomeone else's report by id — a learner outside the caller's team is a\n404, the same answer a nonexistent id gets, so neither reveals the other.\n\n**`counts.assigned` vs `counts.in_progress` splits on PROGRESS, not\nstatus.** A learner who opened a course but completed nothing is\n`in_progress` to the model and \"not started\" to a manager reading a\nprogress bar — this reports the latter, matching both the web page and the\nnative design. `counts.overdue` counts outstanding rows past their due\ndate.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The team member's user id, as returned in the dashboard's `members[].id`.",
            "schema": {
              "type": "integer",
              "example": 41157
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Member retrieved",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/TrainingMyTeamMember"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "unread_notification_count": {
                          "$ref": "#/components/schemas/UnreadNotificationCount"
                        },
                        "_meta": {
                          "$ref": "#/components/schemas/ResponseMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible (`access_denied`), or the caller is not a team lead (`forbidden`)."
          },
          "404": {
            "description": "No such user, or that person is not on the caller's team (error code `not_found` for both)."
          }
        }
      }
    },
    "/training/my_team/{id}/remind": {
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Remind a team member about one course",
        "description": "**Requires the `write:training` scope.** Every first-party token carries\nit (login, refresh and SSO all mint the read/write pair together); a token\nwithout it gets 403 `insufficient_permissions`. Training READS are not\nscope-gated.\n\nNudge ONE enrollment. Per-enrollment rather than per-learner, because that\nis where the affordance sits on both surfaces — a Remind button on each\noutstanding row — and because the web door is per-enrollment too.\n\n**Which email is sent is decided server-side by what can honestly be\nsaid**: a course with a due date gets the due-date reminder (\"due in N\ndays\"); everything else gets the general course reminder. The client does\nnot choose, and there is one implementation behind all three doors (the\ntwo web ones and this) — they previously sent DIFFERENT emails under the\nsame button name, which is why it was consolidated.\n\nDelivery is queued, so a 200 means \"accepted and enqueued\", not\n\"delivered\". `message` is the sentence to show the user and is the same\none the web flashes.\n\nThe enrollment must belong to the named team member; anything else is a\n404 rather than a silent no-op.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The team member's user id.",
            "schema": {
              "type": "integer",
              "example": 41157
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "enrollment_id"
                ],
                "properties": {
                  "enrollment_id": {
                    "type": "integer",
                    "description": "One of the member's own enrollment ids — from this member's `open[].enrollment_id`, or from the dashboard's `overdue[].enrollment_id`. Must belong to this member.",
                    "example": 22989
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reminder enqueued",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "type": "object",
                      "required": [
                        "enrollment_id",
                        "reminded",
                        "message"
                      ],
                      "properties": {
                        "enrollment_id": {
                          "type": "integer",
                          "example": 22989
                        },
                        "reminded": {
                          "type": "boolean",
                          "example": true
                        },
                        "message": {
                          "type": "string",
                          "description": "The sentence to surface to the user.",
                          "example": "Reminder sent to Priya Raman."
                        }
                      }
                    },
                    {
                      "type": "object",
                      "properties": {
                        "unread_notification_count": {
                          "$ref": "#/components/schemas/UnreadNotificationCount"
                        },
                        "_meta": {
                          "$ref": "#/components/schemas/ResponseMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible (`access_denied`), or the caller is not a team lead (`forbidden`)."
          },
          "404": {
            "description": "That person is not on the caller's team, or the enrollment is not theirs (error code `not_found` for both)."
          }
        }
      }
    },
    "/training/courses/{course_id}/reviews/{id}": {
      "patch": {
        "tags": [
          "Training"
        ],
        "summary": "Edit your own course review",
        "description": "**Requires the `write:training` scope.** Every first-party token carries\nit (login, refresh and SSO all mint the read/write pair together); a token\nwithout it gets 403 `insufficient_permissions`. Training READS are not\nscope-gated.\n\nEdit the caller's own review — the native mirror of the web \"Edit your\nreview\" CTA on the ratings block.\n\n**Send only what changes.** `rating`, `title` and `content` are each\napplied only when the key is PRESENT, so a client can patch the star\nrating without resending prose it is not touching. Sending `title` or\n`content` blank is a real change (it clears the field); a blank `rating`\nfails its presence validation rather than silently keeping the old stars.\nSending none of the three is 422 `nothing_to_update`.\n\n**AUTHOR ONLY**, with no time window — the web gate verbatim. A Training\nadmin moderates reviews through the admin surface but may not rewrite\nwords attributed to a learner, so an admin editing someone else's review\nis 403 `forbidden`.\n\n**Not re-run on edit**, both matching the web: the verified-purchase\nbadge (an edit cannot change what was purchased) and `is_approved` — an\nedit does NOT send an approved review back to moderation, which would\nmake it vanish from the list the client is displaying.\n\nThe response carries the **recomputed** `rating` histogram, because\nediting the stars moves it. Payload is identical to the POST's\n(`TrainingReviewWriteResult`).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 37
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The review's own id — as returned in `my_review.id` by the reviews list.",
            "schema": {
              "type": "integer",
              "example": 110
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TrainingReviewUpdateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Review updated",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/TrainingReviewWriteResult"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "unread_notification_count": {
                          "$ref": "#/components/schemas/UnreadNotificationCount"
                        },
                        "_meta": {
                          "$ref": "#/components/schemas/ResponseMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (`access_denied`), or the review belongs to someone else (`forbidden`)."
          },
          "404": {
            "description": "No such course visible to this caller, or no such review on it — including a review that belongs to another course or another tenant (error code `not_found` for all, so none reveals the others)."
          },
          "422": {
            "description": "`nothing_to_update` (no editable key sent) in the standard\n`{ error: { code, message } }` envelope, or a validation failure\n(out-of-range `rating`, over-long `title`/`content`) in the per-field\n`{ errors: [{ field, message, code }] }` shape.\n"
          }
        }
      }
    },
    "/training/learning_paths/{learning_path_id}/reviews/{id}": {
      "patch": {
        "tags": [
          "Training"
        ],
        "summary": "Edit your own learning-path review",
        "description": "**Requires the `write:training` scope.** Every first-party token carries\nit (login, refresh and SSO all mint the read/write pair together); a token\nwithout it gets 403 `insufficient_permissions`. Training READS are not\nscope-gated.\n\nEdit the caller's own review of a learning path. Identical body, payload\nand error codes to the course variant above — reviews are polymorphic, so\none controller serves both (`subject.type` is `path`).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "learning_path_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 1
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 110
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TrainingReviewUpdateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Review updated",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/TrainingReviewWriteResult"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "unread_notification_count": {
                          "$ref": "#/components/schemas/UnreadNotificationCount"
                        },
                        "_meta": {
                          "$ref": "#/components/schemas/ResponseMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible (`access_denied`), or the review is someone else's (`forbidden`)."
          },
          "404": {
            "description": "No such learning path visible to this caller, or no such review on it (`not_found`)."
          },
          "422": {
            "description": "`nothing_to_update`, or a validation failure — see the course variant above."
          }
        }
      }
    },
    "/training/courses/{course_id}/sessions": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "Bookable sessions for an instructor-led course",
        "description": "The \"choose your session\" list — the native counterpart of My Learning →\nan ILT course → the session-list dialog.\n\n**Only bookable rows.** `scheduled` + starting in the future, ordered\nsoonest-first. A past or cancelled session is omitted rather than returned\nwith a closed CTA: you cannot register into either, and a list that led\nwith last month's cohort is the wrong answer to \"which session shall I\ntake\".\n\n**`cta_action` is the row's verb**, resolved server-side with the same\nprecedence the web row uses, so no client re-derives it:\n`registered` · `waitlisted` (a seat you already hold — always wins) ·\n`closed` (registration window shut) · `join_waitlist` (full) ·\n`switch` (you hold a seat on a DIFFERENT session of this course) ·\n`register`.\n\n**`capacity` is computed once for the whole list** in a single grouped\nquery, so a course with a weekly schedule costs no per-row counting.\n`taken` counts occupied seats (registered/attended/completed);\n`waitlist_count` is separate and does not consume capacity.\n\n**Render times in the SESSION's zone.** `starts_at` / `ends_at` are the\ninstants in UTC; `timezone` / `timezone_label` are the zone to show them in,\nwhich is what the web renders — a class happens where it happens, not where\nthe viewer is. The pre-converted `starts_at_local` / `ends_at_local` were\nremoved on 2026-09-01 as derivable from those two.\n\n**`can_register` reflects BOTH switches** — the tenant-wide Training\nsetting AND this course's own `allow_self_enrollment`. When it is false\nthe rows are still returned (so the schedule can be displayed) but\n`POST /training/sessions/{id}/register` will answer 403; disable the\nbutton rather than letting the user discover it on tap.\n\n`my_registration` reports the caller's hold in WHATEVER state, including\n`attended`/`completed`, which the per-row `cta_action` deliberately\nignores — see the note on the register endpoint.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The instructor-led course."
          }
        ],
        "responses": {
          "200": {
            "description": "The bookable session list.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "course": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "title": {
                          "type": "string"
                        },
                        "delivery_mode": {
                          "type": "string"
                        },
                        "instructor_led": {
                          "type": "boolean"
                        }
                      }
                    },
                    "sessions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TrainingSessionRow"
                      }
                    },
                    "my_registration": {
                      "$ref": "#/components/schemas/TrainingSessionRegistrationSummary"
                    },
                    "can_register": {
                      "type": "boolean",
                      "description": "Both self-enrollment switches are on."
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible (`access_denied`)."
          },
          "404": {
            "description": "No such course, or it is not visible to this caller (`not_found`)."
          },
          "422": {
            "description": "The course is not instructor-led, so it has no sessions (`not_instructor_led`)."
          }
        }
      }
    },
    "/training/sessions/{id}": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "One instructor-led session",
        "description": "A single session addressed by its own id — the same row shape the list\nreturns, for a client that holds a session id (from a reminder, a deep\nlink, or a previously fetched list) without its course.\n\nFlat rather than nested under the course for the same reason the `qa/`\nroutes are: the id addresses the row on its own and the course is derived\nfrom it. Visibility is still enforced through the course — a session whose\ncourse has been unpublished or archived answers 404.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The session.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "course": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "title": {
                          "type": "string"
                        },
                        "delivery_mode": {
                          "type": "string"
                        },
                        "instructor_led": {
                          "type": "boolean"
                        }
                      }
                    },
                    "session": {
                      "$ref": "#/components/schemas/TrainingSessionRow"
                    },
                    "my_registration": {
                      "$ref": "#/components/schemas/TrainingSessionRegistrationSummary"
                    },
                    "can_register": {
                      "type": "boolean"
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible (`access_denied`)."
          },
          "404": {
            "description": "No such session in this business, or its course is no longer visible (`not_found`)."
          }
        }
      }
    },
    "/training/sessions/{id}/register": {
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Take (or move) the caller's seat on a session",
        "description": "**Requires the `write:training` scope** (see the Training write-scope note\non the review and Q&A write endpoints). No request body.\n\n**One endpoint covers register, switch AND join-waitlist.** The web has\nthree actions; they differ only in the message they flash. Underneath,\n`Training::SessionRegistrationService#switch` is documented as the same\noperation as `#register` (\"it replaces whatever active hold the learner\nhas for the course\"), and `#register` already falls back to the waitlist\nwhen a session is full. So: POST here to book a seat, to MOVE your seat\nfrom another session of the same course, or to queue for a full one.\n\n**`intent` says what you ASKED for**, which is a different question from\nwhat the seats allow. `register` (the default when the field is absent)\ntakes a seat and falls back to the waitlist if the session is full;\n`waitlist` queues you deliberately even when a seat is free — the two\nbuttons a session row draws (\"Choose this session\" / \"Join waitlist\").\nAn unrecognised value is a 422 `invalid_intent`, NOT a silent fallback:\nguessing `register` for a mistyped `waitlist` would book a seat you never\nasked for. The response echoes `intent`, so `intent: register` alongside\n`status: waitlisted` is exactly how you detect that the last seat went\nbetween the list and the tap.\n\n**Read `status` from the response — do not predict it.** It is\n`registered` or `waitlisted`, decided under a row lock on the session, so\na seat that filled between your GET and your POST cannot over-book. The\nreturned `session` already reflects your write (its `capacity` and\n`cta_action` are re-read afterwards), so a client can re-render the row\nwithout a second request.\n\n**Idempotent.** Posting again for a session you already hold returns that\nsame registration rather than creating a duplicate.\n\n**Registering is also how you ENROLL.** For an instructor-led course the\nservice creates the `TrainingEnrollment` if you have none, applying the\nsame prerequisite gating as catalog enrollment — a prerequisite failure\ncomes back 422 with the service's own message.\n\n**Refusals**, each with a stable `error.code`:\n\n- `self_enrollment_disabled` (403) — either the tenant-wide Training\n  setting or this course's `allow_self_enrollment` is off. `can_register`\n  on the two GETs above reports the same thing in advance.\n- `registration_closed` (422) — the session's registration window has shut.\n- `already_attended` (422) — you have already sat a session for this\n  course. Refused rather than re-booked: moving that hold would discard\n  earned attendance credit. A re-assignment opens a new attempt.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "intent": {
                    "type": "string",
                    "enum": [
                      "register",
                      "waitlist"
                    ],
                    "default": "register",
                    "description": "What the caller is asking for. Omit for the historical behaviour (take a seat, fall back to the waitlist when full)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The seat, and the row re-read after the write.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "registered",
                        "waitlisted",
                        "attended"
                      ],
                      "description": "`attended` is returned by the idempotent path only — you already sat this session, so nothing was booked and your seat is spent. Treat it as \"no change\", not as a new booking."
                    },
                    "waitlisted": {
                      "type": "boolean"
                    },
                    "intent": {
                      "type": "string",
                      "enum": [
                        "register",
                        "waitlist"
                      ],
                      "description": "The intent that was applied, echoed back."
                    },
                    "message": {
                      "type": "string",
                      "description": "The same sentence the web flashes."
                    },
                    "registration": {
                      "$ref": "#/components/schemas/TrainingSessionRegistrationSummary"
                    },
                    "session": {
                      "$ref": "#/components/schemas/TrainingSessionRow"
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible (`access_denied`), the token lacks `write:training` (`insufficient_permissions`), or self-enrollment is off for the tenant or the course (`self_enrollment_disabled`)."
          },
          "404": {
            "description": "No such session in this business, or its course is no longer visible (`not_found`)."
          },
          "422": {
            "description": "`registration_closed`, `already_attended`, or a prerequisite failure from the enrollment step."
          }
        }
      }
    },
    "/training/sessions/{id}/registration": {
      "delete": {
        "tags": [
          "Training"
        ],
        "summary": "Give up the caller's seat, or leave the waitlist",
        "description": "**Requires the `write:training` scope.** No request body.\n\nThe correction path for `POST /training/sessions/{id}/register` — a\nlearner who booked a seat can release it, and one who joined a queue can\nleave it. This is the session row's \"Leave waitlist\" state, and the\ncounterpart of the web's `DELETE .../cancel_registration`.\n\n**DELETE on the REGISTRATION sub-resource, not on the session** — the\nsession is not being removed, the caller's hold on it is.\n\n**Cancelling is what frees a seat**, so the service promotes the head of\nthat session's waitlist inside the same call. No client needs to do that\narithmetic, and no second request is required.\n\n**An `attended` hold cannot be cancelled** — it answers 422\n`no_active_registration` and the record is left intact. Erasing it would\ndiscard the learner's earned attendance credit; the session list reports\nthe seat as attended instead. Same rule the web states outright.\n\nScoped to THIS session: a hold on a different session of the same course\nis not cancelled by this route.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The hold was cancelled, and the row re-read afterwards.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "cancelled": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    },
                    "registration": {
                      "$ref": "#/components/schemas/TrainingSessionRegistrationSummary"
                    },
                    "session": {
                      "$ref": "#/components/schemas/TrainingSessionRow"
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible (`access_denied`), or the token lacks `write:training` (`insufficient_permissions`)."
          },
          "404": {
            "description": "No such session in this business, or its course is no longer visible (`not_found`)."
          },
          "422": {
            "description": "`no_active_registration` — the caller holds nothing cancellable on this session (including the attended case above)."
          }
        }
      }
    },
    "/leader-rounds/due": {
      "get": {
        "tags": [
          "Leader Rounds"
        ],
        "summary": "Rounds due on the caller's direct reports",
        "description": "The caller's due list — every ACTIVE direct report, with how long since\neach was last rounded on and whether that is past the tenant's cadence.\n\n**Direct reports only, and all of them.** A skip-level round can be\nlogged on anyone in the caller's subtree, but it carries no obligation —\notherwise a director would be \"overdue\" on hundreds of people on day\none. Terminated and suspended users never appear.\n\n**Leaders only, but never an error.** A non-leader gets\n`entries: []` and `due_count: 0`, so a client can call this\nunconditionally.\n\nOnly SUBSTANTIVE completed rounds clear a row (a completed round with\nevery answer blank scores nothing — guardrail #1 against rounding\ntheatre), and a recorded skip suppresses the obligation without\npretending the person was rounded on: their entry carries\n`due: false` with `never_rounded: true` and no `last_rounded_on`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Due list retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "cadence_days",
                    "due_count",
                    "entries"
                  ],
                  "properties": {
                    "cadence_days": {
                      "type": "integer",
                      "description": "The tenant's rounding cadence in days (the `default_cadence_days` app setting, clamped to 1..365). Published so a client need not hardcode 30.",
                      "example": 30
                    },
                    "due_count": {
                      "type": "integer",
                      "description": "How many entries carry `due: true`.",
                      "example": 3
                    },
                    "pacing": {
                      "type": "object",
                      "description": "This period's pacing plan for the caller — how many rounds the cadence expects of them in the current window, how many are done, and whether they are on pace. Emitted on every response; previously undocumented.",
                      "properties": {
                        "window": {
                          "type": "string",
                          "example": "2026-09"
                        },
                        "window_label": {
                          "type": "string",
                          "example": "September"
                        },
                        "target": {
                          "type": "integer",
                          "example": 6
                        },
                        "done": {
                          "type": "integer",
                          "example": 2
                        },
                        "remaining": {
                          "type": "integer",
                          "example": 4
                        },
                        "paced": {
                          "type": "boolean",
                          "example": false
                        }
                      }
                    },
                    "entries": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "subject_id",
                          "subject_name",
                          "due",
                          "never_rounded"
                        ],
                        "properties": {
                          "subject_id": {
                            "type": "integer",
                            "example": 1752
                          },
                          "subject_name": {
                            "type": "string",
                            "example": "Marcus Bell"
                          },
                          "last_rounded_on": {
                            "type": "string",
                            "format": "date",
                            "nullable": true,
                            "description": "Null when never rounded.",
                            "example": "2026-07-12"
                          },
                          "days_since": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Null when never rounded.",
                            "example": 37
                          },
                          "due": {
                            "type": "boolean",
                            "description": "Past cadence (or never rounded) AND not suppressed by a recorded skip. This is the flag a client badges on — but badge it in TEXT, not colour alone.",
                            "example": true
                          },
                          "never_rounded": {
                            "type": "boolean",
                            "example": false
                          },
                          "last_skipped_on": {
                            "type": "string",
                            "format": "date",
                            "nullable": true,
                            "description": "When a skip was last recorded for this person, else null. WHY a row is not due is not derivable from the other fields: a recorded skip suppresses the obligation without counting as a round, so a skipped-and-never-rounded subject reads `due: false` / `never_rounded: true` / `last_rounded_on: null` — identical to someone whose cadence simply has not lapsed. A client must render those two differently (\"Never rounded\", no Due badge, because a skip is recorded).",
                            "example": "2026-08-18"
                          },
                          "job_title": {
                            "type": "string",
                            "nullable": true,
                            "description": "The subject's credential, null when the tenant does not record one. How a person is identified on a floor (\"Marcus Bell, RN\") — two Bells on one unit is normal, so a bare name is genuinely ambiguous.",
                            "example": "RN"
                          },
                          "avatar_url": {
                            "type": "string",
                            "nullable": true,
                            "description": "Absolute URL, null when no avatar is set.",
                            "example": "https://acme.workforce.mangoapps.com/rails/active_storage/…"
                          },
                          "scheduled": {
                            "type": "boolean",
                            "description": "True when a recurring rounding meeting carrying a real calendar event exists for this leader/subject pair. DISPLAY ONLY — it never influences due-ness (CalendarPairing feeds nothing in DueList/CoverageReport), so a scheduled round is still owed until it is logged.",
                            "example": true
                          },
                          "scheduled_for": {
                            "type": "string",
                            "format": "date",
                            "nullable": true,
                            "description": "The next non-cancelled occurrence, or null when `scheduled` is true but the series has not materialised that far ahead — a real state, not an error.",
                            "example": "2026-08-25"
                          },
                          "scheduled_pairing_id": {
                            "type": "integer",
                            "nullable": true,
                            "description": "The CalendarPairing id, present only when `scheduled` is true. Pass it to `DELETE /leader-rounds/calendar-pairings/{id}` to remove the standing meeting. A client without this id can only ever ADD a pairing, and the server then refuses a second one with 422 `already_paired`.",
                            "example": 3
                          }
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Leader Rounds is not enabled for the business (`app_disabled`) or not accessible to the caller (`app_forbidden`)."
          }
        }
      }
    },
    "/leader-rounds/templates": {
      "get": {
        "tags": [
          "Leader Rounds"
        ],
        "summary": "Rounding templates and their question sets",
        "description": "The capture form's question set — every ACTIVE template in the tenant\nwith its ordered, typed questions.\n\n**Why this endpoint exists:** `POST /leader-rounds/rounds` takes\n`answers` keyed by question id, and without this there was no way to\nlearn a question id, its prompt, its type, or which questions are\nrequired. The capture form is undrawable without it.\n\n**`field_type` is the field to render on.** It is the app's own\n`question_type` translated into the platform's shared form-field\nvocabulary, so a client drives its existing dynamic form renderer\ninstead of carrying a private translation table:\n\n| `question_type`    | `field_type` | Control |\n|--------------------|--------------|---------|\n| `text`             | `textarea`   | Free text |\n| `scale`            | `rating`     | 1..5, bounds published per question |\n| `boolean`          | `radio`      | Yes / No |\n| `choice`           | `select`     | From `choices` |\n| `recognition_pick` | `lookup`     | Business-user picker, stores a user id |\n| `issue_capture`    | `textarea`   | The issue DESCRIPTION — see below |\n\n**`field_type` is not a complete rendering contract — `inputs` is.**\nTwo question types need MORE THAN ONE control, and `field_type` names\nonly the first:\n\n| `question_type`    | inputs |\n|--------------------|--------|\n| `recognition_pick` | `referenced_user_id` (lookup) + `value` (the citation text) |\n| `issue_capture`    | `description` + `assigned_to_id` + `due_date` + `priority` |\n\nEvery question therefore carries an `inputs` array — one entry for a\nsimple question, several for a composite — plus a `composite` boolean for\nclients that want to branch. Render every entry in `inputs` and a\ncomposite cannot be half-built.\n\nThis is not hypothetical. Before `inputs` existed, the native capture\nform trusted `field_type: lookup` and drew a recognition_pick's person\npicker WITHOUT its citation field. `Answer#recognition_content` builds\nthe recognition post body from that citation, so every recognition posted\nfrom mobile read \"Recognized during a leader round on <date>.\" — a public\ncompliment on a colleague's feed with no reason in it. Nothing in the\npayload revealed the missing half; it took someone comparing the mobile\nand web forms side by side.\n\nEach input names its own `submit_as` (`answers` or `issues`), because\nthat is the other half easily got wrong: an `issue_capture`'s inputs post\nthrough the top-level `issues` hash, not `answers`, and produce a row on\nthe stoplight ledger.\n\n`default_template_id` is the template to open on. It is the\nindustry-NEUTRAL staff template, not the first row: ordering is by\n(round_type, name), so \"first\" would hand a first-time leader either New\nHire 30-60-90 or — on a tenant with an industry pack installed — a\nvariant whose required questions ask about patients and supplies.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Templates retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "templates"
                  ],
                  "properties": {
                    "default_template_id": {
                      "type": "integer",
                      "nullable": true,
                      "description": "The template a client should open on. See above.",
                      "example": 1
                    },
                    "templates": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "name",
                          "round_type",
                          "questions"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 1
                          },
                          "name": {
                            "type": "string",
                            "example": "Staff Rounding"
                          },
                          "description": {
                            "type": "string",
                            "nullable": true
                          },
                          "round_type": {
                            "type": "string",
                            "enum": [
                              "staff",
                              "new_hire",
                              "senior_leader",
                              "custom"
                            ],
                            "example": "staff"
                          },
                          "industry": {
                            "type": "string",
                            "nullable": true,
                            "description": "Set on templates installed from an industry pack; null on the industry-neutral starters.",
                            "example": null
                          },
                          "cadence_days": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Null means no obligation (ad-hoc rounds only).",
                            "example": 30
                          },
                          "questions": {
                            "type": "array",
                            "items": {
                              "$ref": "#/components/schemas/LeaderRoundsTemplateQuestion"
                            }
                          }
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not enabled (`app_disabled`), not accessible (`app_forbidden`), or the caller is not a leader (`leader_access_required`). This is the CAPTURE FORM's question set, so it carries the same leader gate the web capture screen does — a non-leader gets no Leader Rounds tabs in the native shell and could not submit the form anyway."
          },
          "422": {
            "description": "No active rounding template in the tenant (`no_active_template`) — an admin must activate one. A client should surface this rather than opening an empty form."
          }
        }
      }
    },
    "/leader-rounds/rounds": {
      "get": {
        "tags": [
          "Leader Rounds"
        ],
        "summary": "List rounds",
        "description": "Paginated rounds, newest `occurred_on` first.\n\n**`scope` selects the read tier** — the two web rounds surfaces\nexpressed as one param:\n* `mine` (DEFAULT) — rounds the caller LED. The default is fixed for\n  backward compatibility: widening it would hand every existing client\n  rows it never asked for and may assume cannot appear.\n* `visible` — the full read tier: led ∪ subtree ∪ about-you, and the\n  whole tenant for app admins. Without this a DIRECTOR had no API path\n  to the rounds their own leaders logged.\n\n`leader_id` narrows to one leader inside the `visible` tier and implies\nit. A leader id outside the tier is refused (`leader_not_visible`, 403)\nrather than ignored — a silently-dropped filter returns a list that is\nnot what the client asked for while looking like it is.\n\nRows are list-shaped: no answers, no issues. Use\n`GET /leader-rounds/rounds/{id}` for the full record.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "mine",
                "visible"
              ],
              "default": "mine"
            },
            "description": "Read tier. See above."
          },
          {
            "name": "leader_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Narrow to one leader; implies `scope=visible`."
          },
          {
            "name": "window",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 7,
              "maximum": 365
            },
            "description": "Narrow to rounds that OCCURRED in the last N days, so a drill-down from a coverage number keeps describing the period that number described. A blank or out-of-range value means NO date narrowing rather than a default: this list is a history, not a coverage measurement, and \"everything\" is a legitimate answer. The value actually applied is echoed as `window_days` (null when unwindowed)."
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Rounds retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "rounds",
                    "meta"
                  ],
                  "properties": {
                    "rounds": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LeaderRoundsRoundSummary"
                      }
                    },
                    "window_days": {
                      "type": "integer",
                      "nullable": true,
                      "description": "The `window` value actually applied, echoed so a client can tell an out-of-range request from an honoured one. Null when no window narrowing was applied. The `window` parameter description above has always promised this field; it was missing from the schema.",
                      "example": 30
                    },
                    "meta": {
                      "$ref": "#/components/schemas/LeaderRoundsPagination"
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`scope` outside the enum (`invalid_scope`). Note that an explicit `scope` is validated BEFORE `leader_id` upgrades it, so `?scope=bogus&leader_id=5` is still a 400."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible; `leader_id` outside the caller's read tier (`leader_not_visible`); or `scope=visible` requested by a non-leader (`leader_access_required`). `scope=visible` is the Rounds Log tier, and its web twin gates the whole controller on leader access; a non-leader's own rounds are served by `GET /leader-rounds/my`, on the subject tier. `scope=mine` stays open to every caller."
          }
        }
      },
      "post": {
        "tags": [
          "Leader Rounds"
        ],
        "summary": "Capture a round",
        "description": "Record a rounding conversation. Delegates to the SAME service the web\ncapture form uses (`LeaderRounds::RoundCreator`), so every gate holds\nidentically on both doors: the subject must be inside the caller's\nsubtree, a self-round is rejected (coverage would be a\nself-attestation), `occurred_on` cannot be in the future, required\nquestions must be answered, and an `issue_capture` answer opens a\ntracked row on the stoplight ledger with an owner and a due date.\n\n**`answers` is keyed by template question id** — get the ids from\n`GET /leader-rounds/templates`. **`issues` is keyed the same way**, on\nthe `issue_capture` question that raised each one.\n\nSaving may also post a recognition to the picked colleague's feed. That\npost is recorded against the answer, so a re-save cannot spam them.\n\n**Send an idempotency key.** `round.idempotency_key` is a\nclient-generated string, unique per capture attempt. A replay of the\nsame key returns the ORIGINAL round with **200** and `replayed: true`\n(not 201), so a client that retries a timed-out create converges\ninstead of double-posting. This matters more than a duplicate row:\nsaving a round also posts a recognition to a colleague's feed and opens\na `Capa::Action` on the stoplight ledger, so one flaky upload would\notherwise become a duplicate round, a spammed recipient and a phantom\nissue. Scoped to (business, leader) and enforced by a partial unique\nindex, so two concurrent retries of one key still yield one round.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "round"
                ],
                "properties": {
                  "round": {
                    "type": "object",
                    "required": [
                      "subject_id",
                      "template_id",
                      "occurred_on"
                    ],
                    "properties": {
                      "subject_id": {
                        "type": "integer",
                        "description": "Must be inside the caller's reporting subtree, and not the caller.",
                        "example": 1752
                      },
                      "template_id": {
                        "type": "integer",
                        "example": 1
                      },
                      "occurred_on": {
                        "type": "string",
                        "format": "date",
                        "description": "The date the conversation happened. Never in the future.",
                        "example": "2026-08-18"
                      },
                      "duration_minutes": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 480,
                        "nullable": true,
                        "example": 14
                      },
                      "private_notes": {
                        "type": "string",
                        "nullable": true,
                        "description": "Leader tier only — never returned to the subject. See the visibility note at the top of this file."
                      },
                      "idempotency_key": {
                        "type": "string",
                        "nullable": true,
                        "maxLength": 255,
                        "description": "Client-generated, unique per capture attempt. Replaying the same key returns the original round with 200 and `replayed: true` instead of creating a second one. Strongly recommended on mobile.",
                        "example": "a3f1c2e0-9b17-4d55-b0e2-6c8a1f2d4e77"
                      }
                    }
                  },
                  "answers": {
                    "type": "object",
                    "description": "Map of template question id → answer. Populate the field matching the question's type; `value` is accepted for the simple types.",
                    "additionalProperties": {
                      "type": "object",
                      "properties": {
                        "value": {
                          "description": "Scalar answer — string for text/choice, 1..5 integer for scale, boolean for yes/no.",
                          "oneOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "number"
                            },
                            {
                              "type": "boolean"
                            }
                          ]
                        },
                        "referenced_user_id": {
                          "type": "integer",
                          "description": "For a `recognition_pick` question — the colleague picked."
                        }
                      }
                    },
                    "example": {
                      "2": {
                        "value": "Coverage has been steady since the swap flow shipped."
                      },
                      "5": {
                        "value": 4
                      }
                    }
                  },
                  "issues": {
                    "type": "object",
                    "description": "Map of `issue_capture` question id → the tracked item to open on the stoplight ledger.",
                    "additionalProperties": {
                      "type": "object",
                      "required": [
                        "description"
                      ],
                      "properties": {
                        "description": {
                          "type": "string"
                        },
                        "assigned_to_id": {
                          "type": "integer",
                          "nullable": true,
                          "description": "The owner. Never inferred by AI — always an explicit pick."
                        },
                        "due_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "priority": {
                          "type": "string",
                          "nullable": true,
                          "enum": [
                            "low",
                            "medium",
                            "high",
                            "critical"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Round captured",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "round"
                  ],
                  "properties": {
                    "round": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LeaderRoundsRoundSummary"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "answers_count": {
                              "type": "integer",
                              "example": 5
                            },
                            "issues_count": {
                              "type": "integer",
                              "example": 1
                            }
                          }
                        }
                      ]
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "The round SAVED, but part of the submission did not land as sent — e.g. a recognition pick who is no longer active (no recognition recorded), an issue owner who no longer resolves (the issue falls back to the leader and, because a self-assignment is not notified, nobody is told at all), issues past the 10-per-question ceiling, or a due date that was clamped. None of this is visible in the counts, which all look right — so an empty array is the only \"everything landed as sent\" signal, and a client should surface any entry to the leader rather than treating the 201/200 as the whole answer.",
                      "example": []
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "200": {
            "description": "Idempotent replay. The `round.idempotency_key` you sent has already been used by this leader, so the ORIGINAL round is returned and nothing new was created. `replayed` is true. Treat this as success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "round"
                  ],
                  "properties": {
                    "round": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LeaderRoundsRoundSummary"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "answers_count": {
                              "type": "integer",
                              "example": 5
                            },
                            "issues_count": {
                              "type": "integer",
                              "example": 1
                            }
                          }
                        }
                      ]
                    },
                    "replayed": {
                      "type": "boolean",
                      "example": true
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "The round SAVED, but part of the submission did not land as sent — e.g. a recognition pick who is no longer active (no recognition recorded), an issue owner who no longer resolves (the issue falls back to the leader and, because a self-assignment is not notified, nobody is told at all), issues past the 10-per-question ceiling, or a due date that was clamped. None of this is visible in the counts, which all look right — so an empty array is the only \"everything landed as sent\" signal, and a client should surface any entry to the leader rather than treating the 201/200 as the whole answer.",
                      "example": []
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible."
          },
          "422": {
            "description": "Validation failed — subject outside the caller's subtree, a self-round, a future `occurred_on`, a missing required answer, or a scale value outside 1..5. The error code names which."
          }
        }
      }
    },
    "/leader-rounds/rounds/{id}": {
      "get": {
        "tags": [
          "Leader Rounds"
        ],
        "summary": "One round in full",
        "description": "The whole record of one conversation: every answer in template order,\nthe issues it raised, the recognition it posted, and — for the leader\ntier only — the private notes.\n\n**The tier is the server's job.** Whether `private_notes` is in the\npayload AT ALL is decided here, not by the client hiding a section:\non a subject's response the key is ABSENT. `private_notes_visible`\nstates it positively so a client can DRAW the absence (\"your leader's\nprivate notes aren't shown here\") rather than leaving a silent gap.\n\n`private_notes_visible` is true ONLY for the round's own leader — not\nfor a director above them or an app admin, both of whom can read the\nround itself. It tracks whether `private_notes` is in THIS response, not\nwhether the caller is inside the notes tier: a flag that said \"visible\"\nwhile the key was withheld made an empty card read as \"the leader wrote\nnone\", which is a different claim from \"not shown to you\".\nEverything else — the same answers, the same issue rows — is identical\nbetween the two tiers, because it is literally the same record.\n\nA round id outside the caller's read tier and a nonexistent id both\nreturn 404 with the same body. See the enumeration note at the top.\n\nNote an `issue_capture` question produces a LEDGER ROW, not an answer\nrow, so `answers` can be shorter than the template's question count\nwhile `issues` carries the difference.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Round retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "round"
                  ],
                  "properties": {
                    "round": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LeaderRoundsRoundSummary"
                        },
                        {
                          "type": "object",
                          "required": [
                            "answers",
                            "issues",
                            "private_notes_visible"
                          ],
                          "properties": {
                            "duration_minutes": {
                              "type": "integer",
                              "nullable": true,
                              "example": 14
                            },
                            "completed_at": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true
                            },
                            "answers_count": {
                              "type": "integer",
                              "example": 5
                            },
                            "issues_count": {
                              "type": "integer",
                              "example": 1
                            },
                            "private_notes_visible": {
                              "type": "boolean",
                              "description": "False on the subject tier, where `private_notes` is absent from the payload entirely.",
                              "example": true
                            },
                            "answers": {
                              "type": "array",
                              "description": "Ordered by the template's own question order.",
                              "items": {
                                "$ref": "#/components/schemas/LeaderRoundsAnswer"
                              }
                            },
                            "issues": {
                              "type": "array",
                              "description": "The stoplight rows this round raised, oldest first.",
                              "items": {
                                "$ref": "#/components/schemas/LeaderRoundsIssue"
                              }
                            }
                          }
                        }
                      ]
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible."
          },
          "404": {
            "description": "Round not found (`not_found`) — returned identically for a nonexistent id and for one outside the caller's read tier."
          }
        }
      }
    },
    "/leader-rounds/team": {
      "get": {
        "tags": [
          "Leader Rounds"
        ],
        "summary": "Coverage rollup up the reporting line",
        "description": "The coverage report with layer-by-layer drill-down. **The axis is the\nreporting line, not the location tree.** Every aggregate is batched — no\nper-leader queries.\n\nThree collections answer three different questions, and mixing them up\nis the usual misreading:\n* `summary` — the totals for the anchored node, including its own direct\n  reports.\n* `units` — the layer DIRECTLY BELOW the node, each row aggregating that\n  unit's whole subtree. The node's own direct reports sit in `summary`\n  and in no unit row; without knowing that, a reader finds a gap between\n  the two and concludes one is broken.\n* `leaders` — flat per-leader rows for the subtree, paginated. A\n  leader's row counts THEIR OWN direct reports only.\n\nOrdered worst-first (`[-red_issues, coverage_pct]`) so the response opens\non the problem.\n\n**Coverage counts SUBSTANTIVE completed rounds only** — blank-answer\nrounds score nothing (guardrail #1). `same_day_cluster` is guardrail #2:\ntrue when five or more rounds in the window have 80% of them stamped on\none calendar day. Nine rounds inside twenty-two minutes is a signal a\ndirector should see, not something to smooth away.\n\nAgeing is reported as two separate numbers because they answer different\nquestions: `oldest_red_days` is days PAST DUE (the same clock the\nescalation job counts on), `oldest_open_days` is days SINCE RAISED.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "node",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Anchor at one leader in the caller's tree. Blank means no narrowing. An id outside the caller's tree is refused (`node_not_visible`, 403) rather than clamped — on an API a silently-ignored filter produces a mislabelled list."
          },
          {
            "name": "window",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 30,
              "minimum": 7,
              "maximum": 365
            },
            "description": "Measurement window in days. Out-of-range values fall back to 30."
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Rollup retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "window_days",
                    "breadcrumb",
                    "summary",
                    "units",
                    "leaders",
                    "meta"
                  ],
                  "properties": {
                    "window_days": {
                      "type": "integer",
                      "example": 30
                    },
                    "node": {
                      "type": "integer",
                      "nullable": true,
                      "description": "The anchored node, or null for the caller's own root."
                    },
                    "breadcrumb": {
                      "type": "array",
                      "description": "Path from the top of the caller's span down to `node`.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "summary": {
                      "type": "object",
                      "description": "Totals for the anchored node. Includes its own direct reports."
                    },
                    "at_span_roots": {
                      "type": "boolean",
                      "description": "TRUE only for an app admin on the UNNAVIGATED root, where `units` is not an org layer but the UNPARENTED bag — every leader whose manager is unset or is not themselves a leader. It cannot be inferred from `node == null`: a non-admin leader anchored on their own node also has a null node, and for them the units genuinely ARE their reports. A client can use it to explain a long flat list (69 rows on the dev tenant, where 61,122 of 62,107 members have no manager set) instead of letting a director read it as their org chart."
                    },
                    "units": {
                      "type": "array",
                      "description": "The layer directly below the node; each row aggregates its whole subtree.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string",
                            "example": "Priya Raman · 4 East"
                          },
                          "leaf": {
                            "type": "boolean",
                            "description": "No layer below. A leaf row should open the leader detail rather than descending into an empty level."
                          },
                          "leaders": {
                            "type": "integer"
                          },
                          "team_size": {
                            "type": "integer"
                          },
                          "rounded_subjects": {
                            "type": "integer"
                          },
                          "coverage_pct": {
                            "type": "number",
                            "format": "float"
                          },
                          "open_issues": {
                            "type": "integer"
                          },
                          "red_issues": {
                            "type": "integer"
                          },
                          "coverage_delegated_out": {
                            "type": "boolean",
                            "description": "TRUE when EVERY leader rolled up under this unit has handed their rounding to someone above them for this window — obligation counts under the delegate, and the row survives only to keep the unit's open ledger visible. It is NOT derivable from the payload: `coverage_pct` is a literal 0 and `expected` a literal 0 in this state, exactly as they are when the unit simply had nobody due, so a client that renders the two the same way accuses a delegated unit of doing nothing. Render the caption, not the bar — \"Rounding delegated for this window; open issues stay theirs\" versus \"No rounds expected in this window\"."
                          },
                          "avatar_url": {
                            "type": "string",
                            "nullable": true,
                            "description": "The unit's own leader — a unit node IS a person. Null when they are no longer an active member; the row still belongs in the rollup (their team's coverage is real), so a client falls back to initials."
                          }
                        }
                      }
                    },
                    "leaders": {
                      "type": "array",
                      "description": "Per-leader rows for the subtree, worst first, paginated.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "leader_id": {
                            "type": "integer"
                          },
                          "leader_name": {
                            "type": "string"
                          },
                          "leader_job_title": {
                            "type": "string",
                            "nullable": true,
                            "description": "The credential, null when the tenant records none."
                          },
                          "avatar_url": {
                            "type": "string",
                            "nullable": true,
                            "description": "Absolute URL, null when no avatar is set."
                          },
                          "credit_mentions": {
                            "type": "integer",
                            "description": "Credit RAISED in this leader's rounds — a recognition_pick answer naming someone."
                          },
                          "credit_posted": {
                            "type": "integer",
                            "description": "How much of `credit_mentions` actually reached Recognitions. A gap means praise is dying in the form, which is why both numbers are published rather than a ratio."
                          },
                          "team_size": {
                            "type": "integer"
                          },
                          "rounded_subjects": {
                            "type": "integer"
                          },
                          "coverage_pct": {
                            "type": "number",
                            "format": "float"
                          },
                          "open_issues": {
                            "type": "integer"
                          },
                          "red_issues": {
                            "type": "integer"
                          },
                          "coverage_delegated_out": {
                            "type": "boolean",
                            "description": "Same flag as on a unit row, for one leader: their rounding is with someone above them for this window. Their team's obligation counts under the delegate's row; this row survives so their standing reds do not vanish from the rollup. Not derivable — see the unit row's note."
                          },
                          "oldest_open_days": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Days since raised."
                          },
                          "oldest_red_days": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Days PAST DUE."
                          },
                          "same_day_cluster": {
                            "type": "boolean",
                            "description": "Guardrail"
                          }
                        }
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/LeaderRoundsPagination"
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible, or `node` outside the caller's tree (`node_not_visible`)."
          }
        }
      }
    },
    "/leader-rounds/issues": {
      "get": {
        "tags": [
          "Leader Rounds"
        ],
        "summary": "The stoplight issue ledger",
        "description": "Issues raised in rounds inside the caller's READ tier, with their\nderived stoplight colour.\n\n**Colour is DERIVED, never stored:** green is completed, red is\ncancelled or open-past-due, yellow is the open remainder. A cancelled\nissue shares red's colour but is a decision, not limbo — clients should\nlabel it \"Won't fix\" rather than repeating \"Red\".\n\n`color` is validated against the enum case-insensitively; anything else\nis an explicit 400. A wrong-case or off-enum value must never return\nevery colour while the client believes the list is filtered.\n\n`node` narrows to one drill-down unit's subtree, composing INSIDE the\nread tier — it can only remove rows, never widen the tier. It is the\nsame param the rollup uses, so one applied scope reads identically on\nboth surfaces.\n\n**`tally` counts the WHOLE scope, before `color` is applied** — the web\nledger's `Stoplight.sql_tally`, as three COUNTs. A paginated client must\nrender its colour pills from this and never from the rows it holds:\ncounting the fetched array caps every total at `per_page` and drops\ncolours that sort past the first page (open-first ordering puts a closed\nwon't-fix last, so reds are exactly what goes missing). It is also what\nmakes `color` safe to send — the pills keep describing the ledger while\nthe list is a filtered, paginated query. `meta.total` is the size of the\nFILTERED query; `tally.total` is the size of the scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "color",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "red",
                "yellow",
                "green"
              ]
            },
            "description": "Case-insensitive. Off-enum values are a 400, not an unfiltered list."
          },
          {
            "name": "node",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Narrow to one unit's subtree, inside the read tier."
          },
          {
            "name": "leader_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Narrow to ONE leader's rounds — what a per-leader issue count on the rollup links to, so the number and the list it opens describe the same set. Composes on top of the read tier (it can only remove rows). An out-of-tier or nonexistent id is refused IDENTICALLY (403 `leader_not_visible`), because a distinguishable refusal is a leader enumerator. `tally` reflects this narrowing."
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ledger retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "issues",
                    "tally",
                    "meta"
                  ],
                  "properties": {
                    "issues": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LeaderRoundsIssue"
                      }
                    },
                    "tally": {
                      "type": "object",
                      "description": "Stoplight counts over the whole scope, independent of `color` and of pagination. `total` is their sum — the colours are exhaustive and disjoint over the status set, so a separate COUNT could only disagree with them.",
                      "required": [
                        "red",
                        "yellow",
                        "green",
                        "total"
                      ],
                      "properties": {
                        "red": {
                          "type": "integer",
                          "example": 1
                        },
                        "yellow": {
                          "type": "integer",
                          "example": 7
                        },
                        "green": {
                          "type": "integer",
                          "example": 4
                        },
                        "total": {
                          "type": "integer",
                          "example": 12
                        }
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/LeaderRoundsPagination"
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`color` outside the enum (`invalid_color`)."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible, `node` not visible (`node_not_visible`), or `leader_id` outside the caller's read tier (`leader_not_visible`)."
          }
        }
      }
    },
    "/leader-rounds/issues/{id}": {
      "get": {
        "tags": [
          "Leader Rounds"
        ],
        "summary": "One issue, with its transition history",
        "description": "The single-issue read that makes an issue DETAIL possible.\n\n`history` is deliberately absent from the list endpoints — it is a query\nper row — so a detail screen rendered from a list row could never show\nthe trail. It existed on exactly one response (the status change), which\nmeant the history was visible only in the seconds after the caller\nthemselves changed something.\n\nIt arrives as an ENVELOPE key (`history`), a sibling of `issue`, NOT\nnested inside it — the same shape the status write emits. A client that\nreads it off the issue object finds nothing and renders an empty trail\nwith no error.\n\n**The read tier here is WIDER than the ledger's, deliberately.** An issue\nASSIGNED to the caller from a round they cannot see is precisely the case\nthe \"Assigned to you\" surface exists for — a non-leader has no visible\nrounds at all, so scoping this to visible rounds alone would 404 the one\nperson the screen serves. Readable = from a round the caller can see, OR\nassigned to the caller. Neither grants a WRITE: that stays the manageable\nset, and `read_only` on the response states which side the caller is on.\n\nA nonexistent id and an out-of-tier id are refused IDENTICALLY (404\n`not_found`), because a distinguishable refusal is an issue enumerator.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The issue plus its `history`; the issue carries `read_only` and `round_visible`",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "issue",
                    "history"
                  ],
                  "properties": {
                    "issue": {
                      "$ref": "#/components/schemas/LeaderRoundsIssue"
                    },
                    "history": {
                      "$ref": "#/components/schemas/LeaderRoundsIssueHistory"
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible."
          },
          "404": {
            "description": "Not visible to the caller, or no such issue — the two are indistinguishable by design (`not_found`)."
          }
        }
      },
      "patch": {
        "tags": [
          "Leader Rounds"
        ],
        "summary": "Reassign an issue, or move its due date / priority",
        "description": "The issue's OWNER, DUE DATE and PRIORITY — everything about an issue\nexcept its status, which has its own endpoint because only that one\ncarries the won't-fix reason gate.\n\nDelegates to `LeaderRounds::IssueDetailsUpdater`, the same service the\nweb panel uses, so three rules hold on both doors: a due date is\nvalidated (not silently clamped, unlike at creation), the audit note and\nthe field change are written in one transaction, and a request that\nchanges NOTHING is not an event — it returns `changed: false` rather\nthan logging a note saying nothing happened and notifying the \"new\"\nowner who is the old owner.\n\n**WRITE TIER.** Being the issue's owner does not let you reassign it;\nbeing the round's subject does not either. Outside the tier is 404,\nidentical to a nonexistent id.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Send only what is changing — an omitted field is left alone. TOP-LEVEL keys; unlike the web form, this endpoint does not accept them wrapped in `issue`.",
                "properties": {
                  "assigned_to_id": {
                    "type": "integer",
                    "description": "The new owner. Must be an active member of the business."
                  },
                  "due_date": {
                    "type": "string",
                    "format": "date",
                    "nullable": true
                  },
                  "priority": {
                    "type": "string",
                    "enum": [
                      "low",
                      "medium",
                      "high",
                      "critical"
                    ]
                  },
                  "pillar_id": {
                    "type": "string",
                    "nullable": true,
                    "description": "Recategorize the issue under one of the tenant's pillars, overriding the pillar derived from the question that raised it. Three values matter: a pillar id PINS that pillar; the sentinel `\"0\"` pins **Unaligned** (an explicit \"this belongs to no pillar\", which outranks the question's own mapping); an empty string CLEARS the override so the issue falls back to whatever the raising question derives. An id belonging to another tenant is refused, not ignored. Read the result back from the response's `pillar_id` / `pillar_name`.",
                    "example": "4"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Applied. `changed` is false when the payload matched what was already stored — no audit note was written and nobody was notified. The issue carries `read_only` and `round_visible`, because a detail screen folds this reply back into the record it is displaying.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "issue",
                    "changed"
                  ],
                  "properties": {
                    "issue": {
                      "$ref": "#/components/schemas/LeaderRoundsIssue"
                    },
                    "changed": {
                      "type": "boolean",
                      "example": true
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible."
          },
          "404": {
            "description": "Not on the caller's manageable ledger, or no such issue."
          },
          "422": {
            "description": "An unusable owner, date or priority (`invalid`), with the reason in `error.message`."
          }
        }
      }
    },
    "/leader-rounds/issues/{id}/comments": {
      "post": {
        "tags": [
          "Leader Rounds"
        ],
        "summary": "Post a progress note on an issue",
        "description": "A note WITHOUT a status change — the thread on an issue.\n\n**READ tier, deliberately.** The person who raised an issue must be able\nto answer on it and so must the owner it was handed to; a thread only\nthe ledger's leader can write to is a broadcast, not a conversation. The\nstatus itself still moves only on the write tier.\n\nThe other party is notified (a leader's note reaches the raiser, a\nreply reaches the owner), never the author, through the same deduped\nin-app \"leader_rounds\" category as every other notification this app\nsends. A note that landed is never reported as a failure because the\nnotify path raised.\n\nBodies are truncated at 5,000 characters. @mentions of business members\nare resolved.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "properties": {
                  "body": {
                    "type": "string",
                    "description": "The note. Whitespace-only is rejected."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The stored note",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "comment"
                  ],
                  "properties": {
                    "comment": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "author": {
                          "type": "string"
                        },
                        "author_avatar_url": {
                          "type": "string",
                          "nullable": true
                        },
                        "body": {
                          "type": "string"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible."
          },
          "404": {
            "description": "Not readable by the caller, or no such issue."
          },
          "422": {
            "description": "A blank body, or the note could not be stored (`invalid`)."
          }
        }
      }
    },
    "/leader-rounds/issues/{id}/status": {
      "patch": {
        "tags": [
          "Leader Rounds"
        ],
        "summary": "Update an issue's status",
        "description": "Move an issue on the stoplight ledger. Delegates to the same service the\nweb ledger uses (`LeaderRounds::IssueStatusUpdater`), so the guardrails\nhold identically:\n\n* **Cancelling requires a reason, and the reason is PUBLISHED to the\n  person who raised it.** This is what keeps the ledger honest — a\n  leader may say no, but not silently. A cancel without\n  `resolution_notes` is 422 `reason_required`.\n* A closed issue stays reopenable, so a misclicked \"resolved\" is\n  recoverable.\n* The transition writes an audit note atomically with the status.\n\n**WRITE TIER, not read tier.** Being the SUBJECT of the round an issue\ncame from grants read, never write; likewise being the issue's OWNER\ndoes not grant a status write — the status belongs to the leader whose\nledger it is, so the person who raised it always hears back from their\nown leader. An issue outside the write tier is 404, identical to a\nnonexistent one.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "status"
                ],
                "properties": {
                  "status": {
                    "type": "string",
                    "description": "The target status on the CAPA register. Validated against `Capa::Action::VALID_STATUSES` — anything else is a 400.",
                    "enum": [
                      "pending",
                      "in_progress",
                      "completed",
                      "cancelled"
                    ],
                    "example": "completed"
                  },
                  "resolution_notes": {
                    "type": "string",
                    "nullable": true,
                    "description": "REQUIRED when cancelling. Published to the raiser — write it for them to read, not for the audit log."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Status updated; the reply carries the refreshed `history`, and the issue carries `read_only` and `round_visible` — a detail screen folds this reply back into the record it is displaying, so a reply that omitted them made the status control and the link to the round disappear the instant the write succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "issue",
                    "history"
                  ],
                  "properties": {
                    "issue": {
                      "$ref": "#/components/schemas/LeaderRoundsIssue"
                    },
                    "history": {
                      "$ref": "#/components/schemas/LeaderRoundsIssueHistory"
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "An unusable target status."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible."
          },
          "404": {
            "description": "Issue not on the caller's manageable ledger — returned identically for a nonexistent id and one outside the write tier."
          },
          "422": {
            "description": "Cancelling without `resolution_notes` (`reason_required`)."
          }
        }
      }
    },
    "/leader-rounds/my": {
      "get": {
        "tags": [
          "Leader Rounds"
        ],
        "summary": "Rounds about me, what I raised, and what I own",
        "description": "The subject tier — the close-the-loop surface, and the answer to \"what\nhappened to what I said\". Three collections, in the order the web\nrenders them:\n\n* `assigned_issues` — work handed to you from someone else's round.\n  **READ-ONLY by design**, flagged `read_only: true` on every row: the\n  status belongs to the leader whose ledger it is. Each row names who\n  RAISED it, which is the one thing a bare ledger row cannot tell an\n  owner. This is the entire issue-owner persona: at rollout a tenant\n  enables Leader Rounds for nursing first, and support departments own\n  issues long before anyone rounds on them — so an owner commonly has\n  three assigned issues and two empty sections.\n* `issues` — the issues YOU raised, with their current colour. This\n  visibility is the whole point: an issue ledger the workforce cannot\n  see is a notebook, not a rounding program.\n* `rounds` — the completed rounds your leader logged with you. Subject\n  tier, so `private_notes` is absent from every row.\n\n**Empty sections are answered honestly.** A department that has not\nadopted rounding gets genuinely empty `rounds` and `issues`; clients\nshould say so plainly rather than painting a green tick.\n\n**Three collections means three cursors.** `page` walks `rounds`,\n`issues_page` walks `issues`, `assigned_page` walks `assigned_issues`,\nand each carries its own meta — so paging one never disturbs the others.\n`meta` stays bound to `rounds` (the envelope-wide key every other action\nhere emits); `issues_meta` and `assigned_issues_meta` are its twins. All\nthree honour `per_page`.\n\n**`assigned_issues` holds work handed to the caller from rounds they were\nNOT the subject of.** Rounds about the caller are excluded because those\nissues already appear in `issues` — listing them in both reads as two\nseparate items. Each row carries `read_only`, computed per caller: false\nwhen the caller leads (or supervises the leader of) the round it came\nfrom, in which case the honest UI points them at the ledger rather than\nclaiming somebody else owns it.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 1
            },
            "description": "Cursor for `rounds`."
          },
          {
            "name": "issues_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 1
            },
            "description": "Cursor for `issues` (what you raised)."
          },
          {
            "name": "assigned_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 1
            },
            "description": "Cursor for `assigned_issues` (what you own)."
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            },
            "description": "Shared by all three collections."
          }
        ],
        "responses": {
          "200": {
            "description": "Subject-tier view retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "rounds",
                    "issues",
                    "assigned_issues",
                    "meta",
                    "issues_meta",
                    "assigned_issues_meta"
                  ],
                  "properties": {
                    "rounds": {
                      "type": "array",
                      "description": "Completed rounds about the caller. Subject tier — no private notes.",
                      "items": {
                        "$ref": "#/components/schemas/LeaderRoundsRoundSummary"
                      }
                    },
                    "issues": {
                      "type": "array",
                      "description": "Issues the caller raised.",
                      "items": {
                        "$ref": "#/components/schemas/LeaderRoundsIssue"
                      }
                    },
                    "issues_tally": {
                      "type": "object",
                      "description": "The stoplight split over everything the caller has RAISED, counted over the WHOLE set rather than the page — a client that counted `issues` would describe 25 rows, not the ledger. Same contract as `tally` on /leader-rounds/issues.",
                      "required": [
                        "red",
                        "yellow",
                        "green",
                        "total"
                      ],
                      "properties": {
                        "red": {
                          "type": "integer"
                        },
                        "yellow": {
                          "type": "integer"
                        },
                        "green": {
                          "type": "integer"
                        },
                        "total": {
                          "type": "integer"
                        }
                      }
                    },
                    "assigned_issues": {
                      "type": "array",
                      "description": "Issues assigned to the caller. Read-only.",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/LeaderRoundsIssue"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "raised_by_name": {
                                "type": "string",
                                "nullable": true,
                                "description": "The subject of the round this issue came from — who asked for it.",
                                "example": "Marcus Bell"
                              },
                              "read_only": {
                                "type": "boolean",
                                "description": "Always true. A client must not render a status control here; the write tier excludes assignment.",
                                "example": true
                              }
                            }
                          }
                        ]
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/LeaderRoundsPagination"
                    },
                    "issues_meta": {
                      "$ref": "#/components/schemas/LeaderRoundsPagination"
                    },
                    "assigned_issues_meta": {
                      "$ref": "#/components/schemas/LeaderRoundsPagination"
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible."
          }
        }
      }
    },
    "/leader-rounds/rounds/skip": {
      "post": {
        "tags": [
          "Leader Rounds"
        ],
        "summary": "Record a skip",
        "description": "Record an untaken round as a **deliberate decision** rather than a gap\n(\"subject on leave\"). Delegates to `LeaderRounds::RoundSkipper`, the same\nservice the web's skip action calls.\n\nA skip **suppresses the obligation without pretending the person was\nrounded on**: the row stops being due, while `/due` still reports\n`never_rounded: true` with no `last_rounded_on` and a populated\n`last_skipped_on`. The skip itself, with its reason, surfaces in the\nleader's recent-rounds history.\n\n**The reason is required** — a skip with no reason is a gap, not a\ndecision, and the subject can read it. The subject must be inside the\ncaller's reporting subtree (the same scope the capture picker uses), so\na skip can never be filed against someone the create gate would reject.\n\nThe template recorded against the skip is the industry-NEUTRAL staff\ntemplate, never the ordering-first row: a skip filed against\n\"New Hire 30-60-90\" reads on the history row as a new-hire round for a\nten-year veteran.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "subject_id",
                  "skipped_reason"
                ],
                "properties": {
                  "subject_id": {
                    "type": "integer",
                    "description": "Must be inside the caller's reporting subtree.",
                    "example": 1752
                  },
                  "skipped_reason": {
                    "type": "string",
                    "description": "Required. Recorded, and readable by the subject.",
                    "example": "On leave until September."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Skip recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "round"
                  ],
                  "properties": {
                    "round": {
                      "$ref": "#/components/schemas/LeaderRoundsRoundSummary"
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible, or the subject is outside the caller's subtree (`invalid_subject`)."
          },
          "422": {
            "description": "Missing reason (`reason_required`), no active template (`no_active_template`), or a model validation failure (`invalid`)."
          }
        }
      }
    },
    "/leader-rounds/rounds/{id}/recognize": {
      "post": {
        "tags": [
          "Leader Rounds"
        ],
        "summary": "Post a round's recognition to Recognitions",
        "description": "Publish the round's `recognition_pick` answer to the named colleague's\nfeed. Delegates to `LeaderRounds::RecognitionPoster`, shared with the\nweb, which routes through `Recognition::PeerPostCreator` — the canonical\nentrypoint carrying every give-gate (peer access, governance,\nmoderation, approval routing). Never posts to a feed directly.\n\n**ONE-SHOT BY CONSTRUCTION.** An already-posted answer returns **200\nwith `already_posted: true`**, not an error — the whole point of the\ndedupe state is that a retry converges instead of spamming the\nrecipient. `approval_status` is `pending_approval` / `pending_review`\nwhen the tenant's governance routes the post for review.\n\n**Only the leader who held the round may post its recognition** — the\npost carries their name, so a manager in the chain posting on their\nbehalf would misattribute the credit.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "answer_id"
                ],
                "properties": {
                  "answer_id": {
                    "type": "integer",
                    "description": "The round's `recognition_pick` answer.",
                    "example": 18
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Posted, or already posted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "already_posted",
                    "answer"
                  ],
                  "properties": {
                    "already_posted": {
                      "type": "boolean",
                      "description": "True when a prior post already exists — not an error.",
                      "example": false
                    },
                    "approval_status": {
                      "type": "string",
                      "nullable": true,
                      "description": "Set when tenant governance routes the post for review.",
                      "example": "pending_review"
                    },
                    "recipient_name": {
                      "type": "string",
                      "nullable": true,
                      "example": "Amy Okonkwo"
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "The give SUCCEEDED but something around it did not — most importantly the one-shot marker may not have been written, so a client that retries on an empty `warnings` list would double-post to the recipient's feed. Treat a non-empty array as \"posted, do not retry, tell the leader\".",
                      "example": []
                    },
                    "answer": {
                      "$ref": "#/components/schemas/LeaderRoundsAnswer"
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not the round's leader (`not_round_leader`), or app not accessible."
          },
          "404": {
            "description": "Round not found or outside the caller's read tier (identical bodies)."
          },
          "422": {
            "description": "The `answer_id` is not part of this round (`answer_not_found` — note this takes the ANSWER's `id`, not its `question_id`), the question does not collect a recognition (`not_a_recognition_answer`), the answer names nobody (`no_recognition_recipient`), or the recognition could not be posted (`recognition_failed`)."
          }
        }
      }
    },
    "/leader-rounds/rounds/subject-search": {
      "get": {
        "tags": [
          "Leader Rounds"
        ],
        "summary": "Search the people the caller may round on",
        "description": "The capture form's person typeahead, and the picker behind \"round on\nsomeone outside my due list\".\n\n**SCOPE IS THE AUTHORIZATION.** Results are the caller's reporting\nsubtree ∩ active members — the same scope `RoundCreator` and\n`RoundSkipper` resolve against, so the picker and the create gate cannot\ndisagree. A global user search would offer people the gate then rejects,\nwhich reads to the user as the app losing their round.\n\nA blank `q` returns the first page of that scope, so the picker can open\npopulated rather than empty. Page with `page` until `meta.more` is\nfalse — a subtree larger than `per_page` is otherwise unreachable.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Case-insensitive substring match against first name, last name, the two joined as \"First Last\", display name, preferred name, email and job title. Because job title is matched, a query like `RN` returns everyone holding that title, not only people whose name contains it. The joined form is one-directional: `Dana Whitfield` matches, `Whitfield Dana` does not."
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            },
            "description": "1-based page cursor. Results are ordered by first name, last name, id."
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            },
            "description": "Page size. A blank, zero, negative or non-numeric value falls back to the default rather than to the minimum."
          }
        ],
        "responses": {
          "200": {
            "description": "Candidate subjects",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "subjects"
                  ],
                  "properties": {
                    "subjects": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 1752
                          },
                          "name": {
                            "type": "string",
                            "example": "Marcus Bell"
                          },
                          "email": {
                            "type": "string",
                            "example": "marcus@example.com"
                          },
                          "job_title": {
                            "type": "string",
                            "nullable": true,
                            "description": "Null when the tenant records none. Present so a round opened from this picker identifies its subject the same way one opened from /due does.",
                            "example": "RN"
                          },
                          "avatar_url": {
                            "type": "string",
                            "nullable": true,
                            "description": "Absolute URL, null when no avatar is set.",
                            "example": "https://acme.workforce.mangoapps.com/rails/active_storage/…"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Cursor state. `more` is true when this page came back full, i.e. another page is plausibly available. There is deliberately no total — counting the whole subtree on every keystroke is what this endpoint exists to avoid.",
                      "properties": {
                        "page": {
                          "type": "integer",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "example": 25
                        },
                        "more": {
                          "type": "boolean",
                          "example": true
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible."
          }
        }
      }
    },
    "/leader-rounds/rounds/pause-reminders": {
      "post": {
        "tags": [
          "Leader Rounds"
        ],
        "summary": "Pause the weekly due-rounds digest",
        "description": "The leader's own off switch for the Monday digest, for four weeks.\nDelegates to `LeaderRounds::DueReminderPreference`, shared with the web.\n\n**A SNOOZE, NEVER A DISMISS.** `NotificationDelivery#dismiss!` is\npermanent and has no inverse, so wiring a user-facing pause to it would\nbe a one-way trap. `resume-reminders` is the real inverse.\n\n`reminders_enabled` reflects the TENANT-level setting — there is no point\noffering a personal pause for a digest the whole account has turned off,\nso a client should read it before showing the control. It is NOT the\npersonal pause and reads `true` on both sides of one: read `paused` for\nthat.\n\n**The body reports what was written, read back from the ledger** — not\nwhat the request intended. There is no GET for pause state, so this reply\nis a client's only source of truth for it.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Paused",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "reminders_enabled",
                    "paused"
                  ],
                  "properties": {
                    "reminders_enabled": {
                      "type": "boolean",
                      "description": "The tenant-level digest setting, not the personal pause.",
                      "example": true
                    },
                    "paused": {
                      "type": "boolean",
                      "description": "The PERSONAL pause, observed after the write. This is the key that distinguishes a paused leader from an unpaused one — `reminders_enabled` cannot, because it answers a different question.",
                      "example": true
                    },
                    "paused_until": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true,
                      "description": "When the snooze lifts, re-read from the ledger after the write. Null means not currently paused — a snooze already in the past is not a pause.",
                      "example": "2026-09-15T09:29:55.371Z"
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible."
          }
        }
      }
    },
    "/leader-rounds/rounds/resume-reminders": {
      "post": {
        "tags": [
          "Leader Rounds"
        ],
        "summary": "Resume the weekly due-rounds digest",
        "description": "The inverse of `pause-reminders` — clears the snooze. Like `pause`, the\nbody reports the state READ BACK from the ledger rather than the state the\nrequest intended, so a successful resume answers `paused: false` and\n`paused_until: null` because that is what the ledger now says.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Resumed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "reminders_enabled",
                    "paused"
                  ],
                  "properties": {
                    "reminders_enabled": {
                      "type": "boolean",
                      "example": true
                    },
                    "paused": {
                      "type": "boolean",
                      "description": "The personal pause, observed after the write.",
                      "example": false
                    },
                    "paused_until": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true,
                      "example": null
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible."
          }
        }
      }
    },
    "/leader-rounds/rounds/recap": {
      "post": {
        "tags": [
          "Leader Rounds"
        ],
        "summary": "Turn a spoken recap into suggested answers",
        "description": "Extract suggested answers to a template's questions from a leader's\nfree-spoken recap of a rounding conversation.\n\n**TAKES TEXT, NOT AUDIO.** The client transcribes on-device (iOS\n`SpeechDictationManager` / `SFSpeechRecognizer`), which removes the\nupload, the polling and any transcript storage — and works where the\nnetwork does not, which matters for an app used in stairwells and med\nrooms. The web's `rounds/voicenote` route is unaffected: it exists to run\nWhisper on an uploaded clip, which on-device transcription makes\nunnecessary rather than wrong.\n\n**IT NEVER PICKS PEOPLE.** `recognition_pick` and `issue_capture`\nquestions are excluded from the model's question set entirely, so it\ncannot invent a colleague or hand someone else's name a piece of work.\nThose stay manual; their ids come back in `excluded_question_ids` so a\nclient can say so rather than leaving the leader to notice the gap.\n\n**IT WRITES NOTHING.** The response is suggestions the leader edits and\nthen saves through `POST /rounds`. A suggestion the model could not make\nvalid (a scale outside 1..5, an off-list choice, a blank) is dropped\nserver-side rather than returned — a blank field the leader fills in is\nrecoverable; a bad value would 422 at save time and read as the app\nlosing their answers.\n\nDegrades honestly: when the model is unavailable or its output is\nunreadable the response is a 422 whose message tells the leader to type\nthe answers instead.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "template_id",
                  "transcript"
                ],
                "properties": {
                  "template_id": {
                    "type": "integer",
                    "example": 1
                  },
                  "transcript": {
                    "type": "string",
                    "description": "The on-device transcript of the leader's recap.",
                    "example": "Caught up with Marcus for fifteen minutes. Things are going well since the swap flow shipped. On support he's about a four out of five."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Suggestions extracted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "suggestions",
                    "excluded_question_ids"
                  ],
                  "properties": {
                    "suggestions": {
                      "type": "array",
                      "description": "Only questions the recap actually addressed. An omitted question is expected, not a failure.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "question_id": {
                            "type": "integer",
                            "example": 5
                          },
                          "question_type": {
                            "type": "string",
                            "enum": [
                              "text",
                              "scale",
                              "boolean",
                              "choice"
                            ]
                          },
                          "value": {
                            "description": "Typed to the question — string, 1..5 integer, or boolean.",
                            "oneOf": [
                              {
                                "type": "string"
                              },
                              {
                                "type": "number"
                              },
                              {
                                "type": "boolean"
                              }
                            ]
                          }
                        }
                      }
                    },
                    "excluded_question_ids": {
                      "type": "array",
                      "description": "Questions deliberately not filled — the recognition pick and the issue owner stay manual.",
                      "items": {
                        "type": "integer"
                      },
                      "example": [
                        3,
                        4
                      ]
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible, the caller is not a leader (`leader_access_required`) — this is a cost-bearing model call on the capture path, and its web twin is leader-gated — or the tenant has turned AI drafting off (`ai_drafting_disabled`, the `ai_drafting_enabled` app setting an app admin controls). The API is the door a tenant CANNOT close by hiding a button, so it refuses here too; a client should offer the manual form rather than retry."
          },
          "422": {
            "description": "Blank transcript (`transcript_required`), inactive template (`invalid_template`), the model was unavailable / unreadable (`recap_failed`, `recap_unparsable`) — type the answers instead — or the tenant's AI credit cap stopped the call (`ai_usage_blocked`). Render `ai_usage_blocked`'s `message` VERBATIM: unlike the others it carries the tenant's own reason (an admin set `ai_monthly_credit_limit`, or unticked \"Keep AI running past your credit balance\", at /admin/billing), and it is the one 422 here that retrying cannot clear."
          }
        }
      }
    },
    "/leader-rounds/scope-tree": {
      "get": {
        "tags": [
          "Leader Rounds"
        ],
        "summary": "One level of the reporting-line tree (scope picker)",
        "description": "Backs the org-chart scope picker on the ledger and the rollup: one level\nat a time, with a breadcrumb back up.\n\n**THE SHAPE IS DELIBERATELY APP-AGNOSTIC.** Per-node numbers are emitted\nas labelled `metrics[{label, value, tone}]` rather than\n`open_issues`/`red_issues` fields, so the client component that renders\nthis can stay ignorant of Leader Rounds and be reused by the next app\nthat grows a drill-down. Labels are server-authored, so they localize\nserver-side like the rest of the payload.\n\n**Why not just use `/team`.** On the rollup, `/team?node=` already\nreturns this level. But the LEDGER has the same picker and `/issues`\nreturns no tree, so driving it from `/team` would construct a full\ncoverage report plus a paginated per-leader array and discard nearly all\nof it — once per level, on a phone.\n\nA row aggregates its **whole subtree**, matching the rollup's unit rows —\na picker row that counted only a node's own reports would disagree with\nthe tile the user lands on after applying it. Colours come from the same\n`Stoplight` scopes the ledger uses, so the picker's \"Red\" means exactly\nwhat the ledger's red pill means.\n\n`Issues` is the subtree's **total** — resolved and won't-fix included —\nbecause applying the row lands on a ledger whose pills read `All (N)` /\n`Red (M)` over that same subtree, and the two numbers have to agree. It\nis the same pair the web's unit chips carry.\n\n**Children are ordered worst-first** (most red, then most issues, then\nname), with nodes that hold nothing anywhere in their subtree ordered\nLAST and flagged `quiet: true`. At a tenant root this level is every\nleader in the business — 66 of 69 rows read zero on the dev tenant — so\nalphabetical order buried the few that mattered. Quiet nodes are ordered\nlast rather than dropped: on mobile this picker is the only way to reach\na node, and on the rollup a team with no issues is often the one worth\nopening (nobody has rounded it, so it raises nothing). A client may\ncollapse them behind a \"show all\" that states how many it is hiding.\n\n**An empty `children` array means the caller has nobody under them**, and\na client should render no scope launcher at all rather than an empty\nsheet.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "node",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Anchor at one leader in the caller's tree; blank opens at the caller's own default layer. An id outside the tree is refused (`node_not_visible`, 403), never clamped — the picker only ever offers nodes the server just returned, so a bad node means a bug or an enumeration attempt."
          }
        ],
        "responses": {
          "200": {
            "description": "One level of the tree",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "breadcrumb",
                    "children"
                  ],
                  "properties": {
                    "node": {
                      "type": "object",
                      "nullable": true,
                      "description": "The anchored node, or null at the caller's root.",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string"
                        }
                      }
                    },
                    "breadcrumb": {
                      "type": "array",
                      "description": "Path from the top of the caller's span down to `node`.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "children": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LeaderRoundsScopeNode"
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible, or `node` outside the caller's tree."
          }
        }
      }
    },
    "/leader-rounds/calendar-pairings": {
      "post": {
        "tags": [
          "Leader Rounds"
        ],
        "summary": "Schedule recurring rounding with one person",
        "description": "\"Put this on our calendars\" — a recurring calendar event per\n(leader, subject) at the template cadence. Delegates to\n`LeaderRounds::CalendarPairingService`, shared with the web.\n\n**The event is convenience layered on top of the obligation, and can\nnever make coverage lie in either direction** — cadence stays computed\nfrom completed rounds, not from calendar events.\n\nDegrades rather than failing: a missing Calendar licence, a subject\noutside the caller's reports, and an already-paired subject are all\nrefusals with readable messages.\n\n**`invitee_ok: false` means the event exists on the LEADER's calendar but\nthe subject's invite did not persist.** A client must not present that as\n\"on both your calendars\" — never report a degraded result as healthy.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "subject_id",
                  "weekday",
                  "time_of_day"
                ],
                "properties": {
                  "subject_id": {
                    "type": "integer",
                    "example": 1752
                  },
                  "weekday": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 6,
                    "description": "Day of the week the recurring check-in lands on, 0 = Sunday through 6 = Saturday. A day NAME (\"Tuesday\", \"Tue\") is also accepted. Anything else is refused with 422 `invalid_weekday` — it is not coerced.",
                    "example": 2
                  },
                  "time_of_day": {
                    "type": "string",
                    "pattern": "^([01]?[0-9]|2[0-3]):[0-5][0-9]$",
                    "description": "Start time in tenant timezone, 24-hour HH:MM. Refused with 422 `invalid_time_of_day` if unreadable — it is not defaulted to midnight.",
                    "example": "09:00"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Pairing created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "pairing"
                  ],
                  "properties": {
                    "pairing": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 1
                        },
                        "subject_id": {
                          "type": "integer",
                          "example": 1752
                        },
                        "subject_name": {
                          "type": "string",
                          "example": "Marcus Bell"
                        }
                      }
                    },
                    "invitee_ok": {
                      "type": "boolean",
                      "description": "False = the subject's invite failed; do not claim both calendars.",
                      "example": true
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible."
          },
          "422": {
            "description": "Calendar app not enabled, subject not an active direct report, already paired, a `weekday` that is missing or unreadable (`invalid_weekday`), a `time_of_day` that is missing or not 24-hour HH:MM (`invalid_time_of_day`), or the calendar call failed. The two time codes are REFUSALS, not coercions — the service stopped defaulting an unparseable weekday to Sunday and a missing time to midnight, which had booked real recurring midnight-Sunday meetings on two calendars behind a 201. An id that is not an active member of the tenant and an id that is a member but not the caller's direct report answer IDENTICALLY (`invalid_subject`, same body) — the same non-enumeration rule this file states for `?leader_id=`."
          }
        }
      }
    },
    "/leader-rounds/calendar-pairings/{id}": {
      "delete": {
        "tags": [
          "Leader Rounds"
        ],
        "summary": "Remove a recurring rounding meeting",
        "description": "Removes the pairing and its calendar event. A pairing is the leader's\nown; app admins may clean up any — the same tier the web applies.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Removed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean",
                      "example": true
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible."
          },
          "404": {
            "description": "Not found, or not a pairing the caller may remove."
          }
        }
      }
    },
    "/comms_hub/dashboard": {
      "get": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "Communications home dashboard",
        "description": "The native-client mirror of the web Communications home\n(`Apps::NewsFeedController#show`) — the \"Mobile Comms App\" home screen.\n\nEvery number and list is produced by the **same** query object that\nbacks the web page (`NewsFeed::DashboardStats`), so the two surfaces\ncannot drift. Each metric is one memoized, business-scoped, N+1-free\nquery hanging off a single shared visibility base\n(`accessible_by ∩ published ∩ active`).\n\nThe payload carries the seven Home surfaces:\n\n* `counts.unread` — audience-visible published posts the caller has not\n  opened (\"Unread for You\").\n* `counts.pending_acknowledgements` — must-read posts still awaiting the\n  caller's acknowledgement (\"Needs Acknowledgement\").\n* `counts.verified_answers` — question posts that carry a verified\n  answer (\"Verified answers\").\n* `have_your_say` — the freshest open poll the caller has not voted on,\n  or `null`.\n* `latest` — the freshest published posts as glance rows (\"Latest from\n  the company\"); at most 4.\n* `must_read` — the top unacknowledged must-read with company\n  acknowledgement progress, or `null` when the caller is all caught up.\n* `trending_topics` — the most-tagged topics across the last 30 days; at\n  most 5.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Dashboard retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "dashboard"
                  ],
                  "properties": {
                    "dashboard": {
                      "type": "object",
                      "required": [
                        "counts",
                        "latest",
                        "trending_topics"
                      ],
                      "properties": {
                        "counts": {
                          "type": "object",
                          "required": [
                            "unread",
                            "pending_acknowledgements",
                            "verified_answers"
                          ],
                          "properties": {
                            "unread": {
                              "type": "integer",
                              "example": 3
                            },
                            "pending_acknowledgements": {
                              "type": "integer",
                              "example": 1
                            },
                            "verified_answers": {
                              "type": "integer",
                              "example": 7
                            }
                          }
                        },
                        "have_your_say": {
                          "type": "object",
                          "nullable": true,
                          "description": "The surfaced \"Have your say\" open poll, or null when none applies.",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "headline": {
                              "type": "string",
                              "description": "Poll headline, falling back to a stripped/truncated body (≤ 90 chars)."
                            },
                            "closes_at": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true,
                              "description": "When the poll closes; null for a poll with no end date (\"Poll open now\")."
                            },
                            "published_at": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true
                            }
                          }
                        },
                        "latest": {
                          "type": "array",
                          "description": "\"Latest from the company\" glance rows, newest first (≤ 4).",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "headline": {
                                "type": "string",
                                "description": "Post headline, falling back to a stripped/truncated body (≤ 90 chars)."
                              },
                              "author": {
                                "type": "object",
                                "nullable": true,
                                "properties": {
                                  "id": {
                                    "type": "integer"
                                  },
                                  "name": {
                                    "type": "string",
                                    "nullable": true
                                  },
                                  "avatar_url": {
                                    "type": "string",
                                    "nullable": true,
                                    "description": "Absolute avatar URL (null when it cannot be resolved)."
                                  }
                                }
                              },
                              "published_at": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true
                              },
                              "topic": {
                                "type": "string",
                                "nullable": true,
                                "description": "Name of the post's first topic chip, or null."
                              },
                              "comment_count": {
                                "type": "integer",
                                "description": "Active (non-deleted, non-moderated) comment count."
                              },
                              "reaction_count": {
                                "type": "integer"
                              }
                            }
                          }
                        },
                        "must_read": {
                          "type": "object",
                          "nullable": true,
                          "description": "Must-Read status card, or null when the caller is all caught up.",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "headline": {
                              "type": "string",
                              "description": "Post headline, falling back to a stripped/truncated body (≤ 60 chars)."
                            },
                            "expires_at": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true
                            },
                            "expires_in": {
                              "type": "string",
                              "nullable": true,
                              "description": "Human-readable expiry (e.g. \"Expires in 2 days\"), or null when it never expires."
                            },
                            "acknowledged": {
                              "type": "boolean",
                              "description": "Whether the calling user has personally acknowledged this post."
                            },
                            "acknowledgement_count": {
                              "type": "integer",
                              "description": "Company acknowledgement count from the analytics cache (0 until computed)."
                            },
                            "acknowledgement_pct": {
                              "type": "integer",
                              "nullable": true,
                              "description": "Company acknowledgement percentage (acks ÷ audience), or null until the snapshot exists."
                            },
                            "policy": {
                              "type": "object",
                              "nullable": true,
                              "description": "The HR policy this must-read asks the reader to accept, plus THIS\ncaller's acceptance of it, or null when the post names no policy.\nByte-for-byte the same block a feed payload carries (`policy` on\n`GET /feeds/{id}`) — one serializer feeds both — so the card can\ndraw the whole \"Policy you're asked to accept\" row (title, an\naccepted / not-yet-accepted chip, and where to read and accept it)\nwithout a follow-up fetch. Acceptance is tracked separately from\nacknowledging the post; render the two states independently.\n",
                              "properties": {
                                "id": {
                                  "type": "integer"
                                },
                                "title": {
                                  "type": "string"
                                },
                                "status": {
                                  "type": "string",
                                  "enum": [
                                    "draft",
                                    "published",
                                    "archived",
                                    "retired"
                                  ],
                                  "description": "The policy's own lifecycle state, verbatim from the record —\nHrPolicy validates `draft | published | archived | retired`, and\nthis block is emitted for whatever policy the must-read names, so\nall four are reachable. Only `published` is linkable (`url` and\n`acknowledge_url` go null otherwise); keep rendering the title on\nan existing must-read whatever the state says.\n"
                                },
                                "requires_acknowledgment": {
                                  "type": "boolean",
                                  "description": "Whether the policy asks to be accepted at all. When false, name the policy without promising an accept action."
                                },
                                "accepted": {
                                  "type": "boolean",
                                  "description": "Whether this caller has accepted the CURRENT version. False when HR has flagged them for re-acknowledgment, even though an older acceptance row exists."
                                },
                                "accepted_at": {
                                  "type": "string",
                                  "format": "date-time",
                                  "nullable": true,
                                  "description": "When they accepted. Null unless `accepted` is true."
                                },
                                "url": {
                                  "type": "string",
                                  "nullable": true,
                                  "description": "Absolute URL of the mobile Policy Hub screen (reads AND accepts) — the \"Read and accept\" target. Null when the policy is no longer published or Policy Hub isn't reachable by THIS caller, so a client never renders a link that only bounces."
                                },
                                "acknowledge_url": {
                                  "type": "string",
                                  "nullable": true,
                                  "description": "Absolute URL to POST (empty body) to accept the policy in place, so the card can offer the accept action itself. Null whenever that POST would be refused — see the same field on the feed payload for the full rule."
                                },
                                "acknowledge_url_reason": {
                                  "type": "string",
                                  "nullable": true,
                                  "enum": [
                                    "not_published",
                                    "app_unavailable",
                                    "not_applicable",
                                    "esignature_required",
                                    "not_targeted"
                                  ],
                                  "description": "Why there is no in-place accept action — non-null exactly when `acknowledge_url` is null, null otherwise. Same codes and rule as the feed payload's field."
                                }
                              }
                            }
                          }
                        },
                        "trending_topics": {
                          "type": "array",
                          "description": "Most-tagged topics across the last 30 days (≤ 5).",
                          "items": {
                            "type": "object",
                            "properties": {
                              "name": {
                                "type": "string"
                              },
                              "posts": {
                                "type": "integer",
                                "description": "Number of posts tagged with this topic in the last 30 days."
                              }
                            }
                          }
                        }
                      }
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "Cross-app unread notification badge count (piggybacked on every API response).",
                      "example": 5
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token"
          },
          "403": {
            "description": "The Communications app is not enabled for the business or not accessible to the caller"
          }
        }
      }
    },
    "/company-store/config": {
      "get": {
        "tags": [
          "Company Store"
        ],
        "summary": "Company Store bootstrap configuration and capabilities",
        "description": "Called **once** when the module opens. Returns the tenant's Company Store\nconfiguration as *this caller* experiences it, so a client can stop\nhardcoding anything tenant-specific and can gate the manager UI.\n\n### The store kill-switch is reported, not enforced\n\n`store_enabled` is `false` when an admin has paused the store. **This\nendpoint still answers 200** — it is the only one in the namespace that\ndoes. Every screen endpoint (`/catalog`, `/dashboard`, `/points`,\n`/orders`) continues to answer `403 store_disabled`, which is what makes\nthe split useful: a client learns the state here and renders \"your\norganization paused the store\" rather than showing an empty catalog or a\ngeneric auth error.\n\nA paused store reports an **empty** `categories` and `payment_methods` —\nnothing is browsable and nothing is purchasable while it is off — and the\ncategory count query is skipped rather than charged to every request for\nthe duration of the pause.\n\n### Gating the manager UI\n\nRole appears **exactly once**, in `viewer`, and it is the namespace's\nshared card. `viewer.is_manager` is\n`Store::RedemptionApprovalService.can_approve_redemptions?` — the very\npredicate the approvals queue's own door gates on — so a client that hides\nthe Approvals tab on this flag never draws a tab that bounces. It is true\nfor a designated approver-group member, and for a line manager with direct\nreports when no group is configured; it is **not** a `role: manager`\nmembership check.\n\n`viewer.is_store_admin` is a business admin-or-above or a Company Store\napp-admin, and is what offers the item / order management surfaces.\n\nThere is deliberately **no** second `permissions` block restating those\ntwo booleans — a parallel spelling is how one surface starts disagreeing\nwith another about who may approve.\n\n### Categories — the filter sheet, from one source\n\n`categories` powers the catalog **category-filter bottom sheet**. The\nfirst row is always **All Categories** (`key: null`) carrying the true\ntotal; then one row per **enabled** category, *including the ones at 0*,\nso the sheet does not reshuffle as a tenant's stock moves (`has_items`\nexists for a client that wants the web dropdown's behaviour of hiding\nempty rows).\n\nTwo things to get right client-side:\n\n* **`key` is the value `?category=` takes verbatim**, not the\n  merchandising label — the `StoreItem` column vocabulary: `swag`,\n  `gift_card`, `experience`, `charitable`. The labels deliberately differ\n  from the keys (`charitable` is merchandised as **\"Donate\"**, and the\n  other three are pluralised), which is why both are sent.\n* **`icon` is a stable FontAwesome-style key**, never an image URL —\n  `tshirt`, `gift`, `star`, `hand-holding-heart`, with `tag` on the All\n  row and `box` as the fallback. These are the same keys the web\n  storefront renders, from\n  `Apps::CompanyStoreHelper#company_store_category_icon`, so an icon\n  change lands on every surface at once.\n\nA category the admin switched off is **absent** from the array entirely —\nit is not sent with `count: 0`. (`count: 0` means the category is on and\nhas no stock right now, which is a different thing and wants different\ncopy.)\n\n### Payment methods — the tenant's ceiling\n\n`payment_methods` lists **only the methods the tenant actually accepts**,\nin the order a client should offer them. Render the array verbatim rather\nthan filtering it: a method a client has never heard of still appears, and\none the admin switched off cannot.\n\n| `key` | `label` | when it appears |\n|---|---|---|\n| `points` | Points | `points_redemption_enabled` |\n| `cash` | Card | `cash_purchases_enabled` |\n| `mixed` | Points + Card | **all three** of the above plus `mixed_payments_enabled` |\n\nThe `mixed` rule is the one that bites. `features.mixed_payments_enabled`\nis the raw admin toggle; a split payment can only be *taken* when the\nstore can take both halves, so a tenant with the mixed toggle on but card\npurchases off gets **no** `mixed` method — exactly as the item detail\nsheet's `payment_options` refuses it. A client that reads the raw feature\nflag instead of this array will draw a split-payment button whose checkout\nbounces.\n\nThis is the **tenant's ceiling**, not a per-item answer: an individual\nitem may be points-only or card-only, and\n`GET /company-store/catalog/{id}` carries the per-item `payment_options`\nwith its own refusal reason for each. No item can ever offer a method\noutside this array.\n\n### The currency noun\n\n`currency_label` (`\"points\"`) and `currency_label_singular` (`\"point\"`)\nare sent so no client hardcodes the word. Both are constants today —\nthere is no per-tenant setting for them yet — but serving them from here\nmeans the day one appears, no client needs a release. The singular is\ncarried alongside the plural because a client rendering \"1 points\" has no\nway to derive it.\n\n### Redemption rules — disclose before, don't surprise at checkout\n\nEvery figure in `redemption` is **null when off**, never `0`, so a client\nnever renders \"cap: 0\" as \"you may redeem nothing\".\n\n* `approval_threshold_points` — the points figure **at or above** which a\n  redemption is HELD for approval instead of spending immediately\n  (checkout compares `>=`). It is the **lowest enabled** of the admin and\n  manager tiers, because that is the one that actually holds; a tier set\n  to `0` is off and is ignored. `null` = no tier enabled.\n* `monthly_points_cap` / `monthly_points_remaining` — the per-user\n  calendar-month ceiling and what is left of it for **this caller**. The\n  remaining figure counts the same set `Store::CheckoutService` counts\n  (points and mixed orders, cancelled and refunded excluded), and is\n  floored at 0. The query behind it is skipped entirely when no cap is\n  configured — which is every tenant that never set one.\n* `velocity_*` — the short-window fraud brake. A breach does **not** block\n  the redemption, it forces the approval hold, so disclose it as \"this may\n  need approval\" rather than as a refusal. `velocity_window_hours` reports\n  the service's own 24h fallback when unset.\n\n### Points\n\n`points` is the caller's **own** wallet, for every role — there is no\npersona branch here. Keys are the plain nouns a config payload reads best\nwith (`balance`, `pending`, `lifetime_earned`, `lifetime_spent`); the\nnamespace's `balance_card` spelling (`points_balance`, `pending_points`,\n…) is still served verbatim by every screen endpoint.\n\n`expiring_points` is what **newly** expires within `expiring_within_days`,\nnever the whole expirable pool. Read it with\n`features.points_expiry_enabled`: `0` means \"nothing is close\" when expiry\nis on, and \"this tenant does not expire points\" when it is off — the\nsecond should render no expiry banner at all.\n\n### Regions\n\n`region` is the region this payload was scoped to (`null` when regions are\noff, or when they are on and this user resolves to none — in which case\nthe pool narrowed to global items; `features.regions_enabled`\ndistinguishes the two). `available_regions` is the vocabulary for a\nclient's own picker, with `active` marking the resolved one. Passing\n`?region_id=` scopes the category counts to a sibling region, exactly like\nthe web region picker.\n\n### Query budget\n\nConstant. Nothing in this payload scales with the number of categories,\nitems, regions or people — the whole category sheet is ONE grouped query,\nand the two per-caller figures that need their own query (the expiring\nslice and this month's committed spend) are skipped entirely unless the\ntenant configured that feature.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "region_id",
            "in": "query",
            "required": false,
            "description": "Scope the category counts to a sibling region, like the web region picker. Ignored when the tenant has no active regions. An id that is not an active region of this tenant falls back to the caller's own resolved region rather than erroring.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The tenant's Company Store configuration as this caller experiences it. Also the answer for a PAUSED store, reported as `store_enabled: false` rather than as a 403.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "config": {
                      "type": "object",
                      "required": [
                        "store_enabled",
                        "store_label",
                        "currency_label",
                        "currency_label_singular",
                        "viewer",
                        "features",
                        "region",
                        "available_regions",
                        "categories",
                        "payment_methods",
                        "points",
                        "redemption"
                      ],
                      "properties": {
                        "store_enabled": {
                          "type": "boolean",
                          "description": "The tenant's store kill-switch. False = an admin paused the store; `categories` and `payment_methods` are then empty and every screen endpoint answers 403 `store_disabled`. Reported rather than enforced — see the endpoint description."
                        },
                        "store_label": {
                          "type": "string",
                          "description": "The app's own name, so a console rename needs no client release. Defaults to \"Company Store\".",
                          "example": "Company Store"
                        },
                        "currency_label": {
                          "type": "string",
                          "description": "What the tenant calls a unit of store currency (plural).",
                          "example": "points"
                        },
                        "currency_label_singular": {
                          "type": "string",
                          "description": "The singular form, sent because a client rendering \"1 points\" has no way to derive it.",
                          "example": "point"
                        },
                        "viewer": {
                          "$ref": "#/components/schemas/CompanyStoreConfigViewer"
                        },
                        "features": {
                          "$ref": "#/components/schemas/CompanyStoreConfigFeatures"
                        },
                        "region": {
                          "$ref": "#/components/schemas/CompanyStoreConfigRegion"
                        },
                        "available_regions": {
                          "type": "array",
                          "description": "Every active region, for a client's own picker. Empty when the tenant has configured none.",
                          "items": {
                            "$ref": "#/components/schemas/CompanyStoreConfigAvailableRegion"
                          }
                        },
                        "categories": {
                          "type": "array",
                          "description": "The catalog category-filter sheet — \"All Categories\" first, then one row per ENABLED category. Byte-for-byte the same rows GET /company-store/catalog/categories returns. Empty while the store is paused.",
                          "items": {
                            "$ref": "#/components/schemas/CompanyStoreConfigCategory"
                          }
                        },
                        "payment_methods": {
                          "type": "array",
                          "description": "The methods this TENANT accepts, in offer order. Only enabled methods appear — render verbatim rather than filtering. Empty while the store is paused, or when the tenant takes neither points nor cash.",
                          "items": {
                            "$ref": "#/components/schemas/CompanyStoreConfigPaymentMethod"
                          }
                        },
                        "points": {
                          "$ref": "#/components/schemas/CompanyStoreConfigPoints"
                        },
                        "redemption": {
                          "$ref": "#/components/schemas/CompanyStoreConfigRedemption"
                        }
                      }
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "Native app badge count (the shared api/v1 envelope)."
                    }
                  }
                },
                "example": {
                  "config": {
                    "store_enabled": true,
                    "store_label": "Company Store",
                    "currency_label": "points",
                    "currency_label_singular": "point",
                    "viewer": {
                      "id": 42,
                      "name": "Anup Kumar",
                      "image": "https://officechat.workforce.mangoapps.com/rails/active_storage/…",
                      "is_manager": true,
                      "is_store_admin": false
                    },
                    "features": {
                      "points_redemption_enabled": true,
                      "cash_purchases_enabled": false,
                      "mixed_payments_enabled": false,
                      "recognition_integration_enabled": true,
                      "regions_enabled": false,
                      "points_per_dollar": 100,
                      "points_expiry_enabled": false
                    },
                    "region": null,
                    "available_regions": [],
                    "categories": [
                      {
                        "key": null,
                        "label": "All Categories",
                        "icon": "tag",
                        "color": "secondary",
                        "count": 34,
                        "active": true,
                        "has_items": true
                      },
                      {
                        "key": "swag",
                        "label": "Swag",
                        "icon": "tshirt",
                        "color": "primary",
                        "count": 14,
                        "has_items": true,
                        "active": false
                      },
                      {
                        "key": "gift_card",
                        "label": "Gift Cards",
                        "icon": "gift",
                        "color": "success",
                        "count": 9,
                        "has_items": true,
                        "active": false
                      },
                      {
                        "key": "experience",
                        "label": "Experiences",
                        "icon": "star",
                        "color": "info",
                        "count": 5,
                        "has_items": true,
                        "active": false
                      },
                      {
                        "key": "charitable",
                        "label": "Donate",
                        "icon": "hand-holding-heart",
                        "color": "warning",
                        "count": 6,
                        "has_items": true,
                        "active": false
                      }
                    ],
                    "payment_methods": [
                      {
                        "key": "points",
                        "label": "Points",
                        "description": "Redeem with your points balance"
                      }
                    ],
                    "points": {
                      "balance": 4200,
                      "pending": 300,
                      "lifetime_earned": 12000,
                      "lifetime_spent": 7800,
                      "last_earned_at": "2026-08-19T10:14:02Z",
                      "last_spent_at": "2026-07-30T16:02:44Z",
                      "points_per_dollar": 100,
                      "expiring_points": 0,
                      "expiring_within_days": 14
                    },
                    "redemption": {
                      "approval_threshold_points": 5000,
                      "monthly_points_cap": null,
                      "monthly_points_remaining": null,
                      "velocity_window_hours": 24,
                      "velocity_max_orders": null,
                      "velocity_max_points": null
                    }
                  },
                  "unread_notification_count": 3
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "`insufficient_permissions` — the token lacks `read:company_store`.\nChecked first, before the app gate, so this answer can precede\n`access_denied`.\n\n`access_denied` — the Company Store app isn't enabled for this tenant,\nor this user is outside the app's audience.\n\nNote this endpoint does **not** answer `store_disabled`. A paused\nstore is a 200 with `store_enabled: false`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "access_denied"
                          ]
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/company-store/dashboard": {
      "get": {
        "tags": [
          "Company Store"
        ],
        "summary": "Company Store dashboard",
        "description": "Everything the Dashboard tab renders in ONE request — the native mirror\nof the web Company Store dashboard (`/apps/company-store`) and of the\nDashboard tab in the Company Store Mobile design.\n\nEvery number and list comes from the same query object the web page uses\n(`Store::DashboardStats`), so the API cannot drift from the page: one\npool for every grid and count (region + category-enablement + audience\nfilters), memoized readings, and lazy invocation so an employee request\nnever runs the manager-only approval queries.\n\n### What is always present\n\n`viewer`, `features`, `region`, `balance`, `stats`, `featured_items`,\n`featured_absorbed`, `saved_items`, `points_activity` and\n`recent_orders`.\n\n### What is conditional\n\nKeys that are gated off are **absent**, not null — exactly as the web\npage renders no card at all. `viewer` and `features` tell a client which\nshape it received, so it never has to infer a disabled surface from a\nmissing key.\n\n* **`within_reach`** — the affordability carousel. Present only when the\n  surface exists (points redemption on, and a balance to spend). An\n  EMPTY array then means \"nothing cheap enough yet\", which is a\n  different message from a tenant that does not redeem points at all.\n* **`earn_onramp`** — the first-run \"how to earn\" card. Present only for\n  a caller with nothing earned, nothing pending and nothing to spend,\n  AND only when Recognitions is actually reachable for them — its CTA\n  deep-links there, so a link that would bounce is not offered.\n* **`team_approvals`** — the design's manager-only widget. Present only\n  for a caller who actually HAS a redemption approval queue, which is the\n  same rule (`Store::RedemptionApprovalService.can_approve_redemptions?`)\n  the queue page's own gate and the app nav read, so this widget never\n  counts holds that page would refuse. It is also exactly what\n  `viewer.is_manager` reports.\n\n`featured_absorbed` is true when the Featured grid came back empty ONLY\nbecause \"Within reach\" is already showing those items — a client's empty\nstate can then say where they went instead of claiming there are none.\n",
        "operationId": "companyStoreDashboard",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "region_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Scope the pool to a sibling region, exactly like the web region picker. Ignored when the tenant has no active regions. The pool is always the selected region PLUS the global items."
          }
        ],
        "responses": {
          "200": {
            "description": "The dashboard",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "dashboard": {
                      "type": "object",
                      "properties": {
                        "viewer": {
                          "$ref": "#/components/schemas/CompanyStoreViewer"
                        },
                        "features": {
                          "$ref": "#/components/schemas/CompanyStoreFeatures"
                        },
                        "region": {
                          "$ref": "#/components/schemas/CompanyStoreRegion"
                        },
                        "balance": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/CompanyStoreBalance"
                            },
                            {
                              "type": "object",
                              "properties": {
                                "expiring_points": {
                                  "type": "integer",
                                  "description": "What NEWLY expires inside `expiring_within_days`. 0 when the tenant does not expire points."
                                },
                                "expiring_within_days": {
                                  "type": "integer",
                                  "description": "The same horizon the web banner and the warning notification use, so the number never disagrees with the copy beside it."
                                }
                              }
                            }
                          ]
                        },
                        "stats": {
                          "type": "object",
                          "description": "The four tappable stat tiles.",
                          "properties": {
                            "available_items": {
                              "type": "integer"
                            },
                            "total_orders": {
                              "type": "integer"
                            },
                            "lifetime_earned": {
                              "type": "integer"
                            },
                            "lifetime_spent": {
                              "type": "integer"
                            }
                          }
                        },
                        "featured_items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/CompanyStoreItemCard"
                          }
                        },
                        "featured_absorbed": {
                          "type": "boolean"
                        },
                        "within_reach": {
                          "type": "array",
                          "description": "ABSENT when points redemption is off or there is no balance to spend. Empty means \"nothing cheap enough yet\".",
                          "items": {
                            "$ref": "#/components/schemas/CompanyStoreItemCard"
                          }
                        },
                        "saved_items": {
                          "type": "object",
                          "description": "The wishlist preview plus its TRUE total, so \"+N more saved\" can be accurate.",
                          "properties": {
                            "total": {
                              "type": "integer"
                            },
                            "items": {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/CompanyStoreItemCard"
                              }
                            }
                          }
                        },
                        "points_activity": {
                          "type": "object",
                          "description": "`period_days` is reported rather than assumed, so a client's header cannot claim a window the server did not compute.",
                          "properties": {
                            "period_days": {
                              "type": "integer"
                            },
                            "earned": {
                              "type": "integer"
                            },
                            "spent": {
                              "type": "integer"
                            },
                            "adjustments": {
                              "type": "integer"
                            },
                            "transaction_count": {
                              "type": "integer"
                            }
                          }
                        },
                        "recent_orders": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/CompanyStoreOrderRow"
                          }
                        },
                        "earn_onramp": {
                          "type": "object",
                          "description": "ABSENT unless this is a first-run caller AND Recognitions is reachable for them.",
                          "properties": {
                            "headline": {
                              "type": "string"
                            },
                            "body": {
                              "type": "string"
                            },
                            "recognition_url": {
                              "type": "string"
                            }
                          }
                        },
                        "team_approvals": {
                          "type": "object",
                          "description": "ABSENT unless `viewer.is_manager`. `pending_count` is the FULL queue depth, not the feed length — the feed is capped and a \"View All\" needs the real number.",
                          "properties": {
                            "pending_count": {
                              "type": "integer"
                            },
                            "queue_url": {
                              "type": "string"
                            },
                            "requests": {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/CompanyStoreApprovalRequest"
                              }
                            }
                          }
                        }
                      }
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "Native app badge count (the shared api/v1 envelope)."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token"
          },
          "403": {
            "description": "`insufficient_permissions` — the token lacks `read:company_store` (checked first, before the app gate). `access_denied` — the Company Store app is not accessible to this caller. `store_disabled` — the tenant's admin has switched the store off.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreError"
                }
              }
            }
          }
        }
      }
    },
    "/company-store/catalog": {
      "get": {
        "tags": [
          "Company Store"
        ],
        "summary": "Browse the catalog",
        "description": "The catalog grid — the native mirror of the web catalog\n(`/apps/company-store/catalog`) and its `/m/` twin, and of the Catalog\nand Featured Items screens in the Company Store Mobile design.\n\nOne request returns everything the screen needs: the page of items, the\nwhole filter sheet (category rows with counts, collection chips, the\nsort menu), the caller's wallet, the tenant's payment toggles, the\nresolved region and the region picker's options. A client should not\nneed a second call to render the screen.\n\n### Filtering, sorting and paging\n\nEvery filter is optional and they all compose. `category`, `sort`,\n`collection`, `search`, `region_id` and `page`/`per_page` behave exactly\nas they do on the web page, because the same query object applies them.\n\nAn unrecognised `category` or `sort` is **dropped**, not rejected and\nnot left to silently empty the grid — and `filters.applied` reports what\nwas actually applied, so a client can see its parameter was ignored.\n\n`collection` is the exception, because collections are free-form\nper-tenant tags with no fixed vocabulary to validate against: an\nunknown one is **applied** and returns an empty grid rather than being\ndropped, and `filters.applied.collection` echoes it back. Read\n`filters.collections` for the collections that actually hold an item.\n\nA non-numeric `page` is page 1; a non-numeric or zero `per_page` falls\nback to the default of 20 rather than to 1.\n\n`featured=true` is **not** the same as `sort=featured`: the filter\nreturns only featured items (the design's Featured Items screen), while\nthe sort orders featured rows first and still returns everything.\n\n### What is in the pool\n\nOnly `available` items — active, and either unlimited inventory or some\nleft. On top of that, three narrowings apply to the grid AND to every\ncount in `filters`, because a count drawn from a wider pool advertises\nresults the grid cannot produce:\n\n* **Category enablement** — an admin can switch a whole category off.\n  Experiences and Donate are OFF out of the box.\n* **Region** — when the tenant has active regions, the pool is the\n  requested (or the caller's own resolved) region PLUS the global items.\n* **Audience targeting** — an item restricted to a recipient group is\n  absent for everyone outside it, and 404s on deep link.\n\n### Prices are gated, not just reported\n\n`points_price` is present only while points redemption is on AND the\nitem carries one; `cash_price` only while card purchases are on (which\nis OFF by default) AND the item carries one. An item with no open\npayment path reports both as `null` and `price_label` as\n`\"Not currently redeemable\"`. This is the same rule the web card\napplies, and it exists so an item can never advertise a price the store\nwould refuse to take.\n\n### `can_quick_redeem`\n\nTrue only when the server would actually accept a one-tap redemption:\npoints on, a points price, an enabled category, in stock, no variants to\npick, no address to collect, no engraving to collect, and affordable on\ntoday's balance. It is the same guard set the redeem endpoint enforces,\nso a client that trusts it never shows a button that bounces.\n",
        "operationId": "browseCompanyStoreCatalog",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "swag",
                "gift_card",
                "experience",
                "charitable"
              ]
            },
            "description": "Narrow to one category. An unrecognised value is dropped. A category the tenant has switched off yields an empty grid, which is the same answer the web page gives."
          },
          {
            "name": "collection",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Narrow to one merchandised collection (\"travel\", \"5-year-anniversary\", …). Case- and whitespace-insensitive, matching how collections are stored."
          },
          {
            "name": "featured",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "Featured items only — the design's Featured Items screen."
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Free text over item name and description. A whitespace-only term is treated as no search. Also applied to every count in `filters`, because each filter row preserves the active term."
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "featured",
                "popular",
                "price_low",
                "price_high",
                "name",
                "newest"
              ],
              "default": "featured"
            },
            "description": "The same six strategies the desktop sort dropdown offers. `popular` ranks by redemptions plus purchases. An unrecognised value falls back to `featured`."
          },
          {
            "name": "region_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Browse a sibling region, exactly like the web region picker. Ignored when the tenant has no active regions, or when the id is unknown (in which case the caller's own resolved region is used). The pool is always the selected region PLUS the global items."
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            },
            "description": "Clamped to 50. A non-numeric or zero value falls back to 20."
          }
        ],
        "responses": {
          "200": {
            "description": "A page of the catalog, with the filter sheet and the caller's context",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "catalog": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/CompanyStoreContext"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "filters": {
                              "$ref": "#/components/schemas/CompanyStoreFilters"
                            },
                            "items": {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/CompanyStoreItemCard"
                              }
                            },
                            "meta": {
                              "$ref": "#/components/schemas/CompanyStorePageMeta"
                            }
                          }
                        }
                      ]
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "Native app badge count (the shared api/v1 envelope)."
                    }
                  }
                },
                "example": {
                  "catalog": {
                    "viewer": {
                      "id": 412,
                      "name": "Priya Raman",
                      "image": "https://acme.workforce.mangoapps.com/rails/active_storage/blobs/redirect/ey/avatar.jpg",
                      "is_manager": false,
                      "is_store_admin": false
                    },
                    "features": {
                      "points_redemption_enabled": true,
                      "cash_purchases_enabled": false,
                      "mixed_payments_enabled": false,
                      "recognition_integration_enabled": true,
                      "regions_enabled": true,
                      "points_per_dollar": 100
                    },
                    "region": {
                      "id": 3,
                      "name": "United States",
                      "country_code": "US"
                    },
                    "balance": {
                      "points_balance": 6250,
                      "pending_points": 300,
                      "lifetime_points_earned": 24800,
                      "lifetime_points_spent": 18550,
                      "last_earned_at": "2026-08-15T08:00:00Z",
                      "last_spent_at": "2026-08-18T10:19:00Z"
                    },
                    "redemption": {
                      "approval_threshold_points": 5000
                    },
                    "available_regions": [
                      {
                        "id": 3,
                        "name": "United States",
                        "country_code": "US",
                        "active": true
                      },
                      {
                        "id": 4,
                        "name": "United Kingdom",
                        "country_code": "GB",
                        "active": false
                      }
                    ],
                    "filters": {
                      "applied": {
                        "category": "swag",
                        "collection": null,
                        "featured": false,
                        "search": null,
                        "sort": "featured",
                        "region_id": 3
                      },
                      "sort_options": [
                        {
                          "key": "featured",
                          "label": "Featured",
                          "active": true
                        },
                        {
                          "key": "popular",
                          "label": "Most Popular",
                          "active": false
                        },
                        {
                          "key": "price_low",
                          "label": "Points: Low to High",
                          "active": false
                        },
                        {
                          "key": "price_high",
                          "label": "Points: High to Low",
                          "active": false
                        },
                        {
                          "key": "name",
                          "label": "Name A-Z",
                          "active": false
                        },
                        {
                          "key": "newest",
                          "label": "Newest",
                          "active": false
                        }
                      ],
                      "categories": [
                        {
                          "key": null,
                          "label": "All Categories",
                          "icon": "tag",
                          "color": "secondary",
                          "count": 20,
                          "active": false,
                          "has_items": true
                        },
                        {
                          "key": "swag",
                          "label": "Swag",
                          "icon": "tshirt",
                          "color": "primary",
                          "count": 6,
                          "active": true,
                          "has_items": true
                        },
                        {
                          "key": "gift_card",
                          "label": "Gift Cards",
                          "icon": "gift",
                          "color": "success",
                          "count": 6,
                          "active": false,
                          "has_items": true
                        }
                      ],
                      "collections": [
                        {
                          "key": "travel",
                          "label": "Travel",
                          "count": 4,
                          "active": false
                        }
                      ]
                    },
                    "items": [
                      {
                        "id": 1,
                        "name": "Company Logo Hoodie",
                        "description": "Soft fleece pullover hoodie with an embroidered company logo.",
                        "category": "swag",
                        "category_label": "Swag",
                        "category_icon": "tshirt",
                        "category_color": "primary",
                        "collection": null,
                        "image_url": "https://acme.workforce.mangoapps.com/rails/active_storage/blobs/redirect/ey/hoodie.jpg",
                        "featured": true,
                        "status": "active",
                        "status_label": "Active",
                        "available": true,
                        "low_stock": false,
                        "inventory_count": null,
                        "requires_shipping": true,
                        "region": {
                          "id": 3,
                          "name": "United States"
                        },
                        "points_price": 4500,
                        "cash_price": null,
                        "price_label": "4500 pts",
                        "redeemable_with_points": true,
                        "purchasable_with_cash": false,
                        "url": "https://acme.workforce.mangoapps.com/apps/company-store/catalog/1",
                        "affordable": true,
                        "points_to_go": 0,
                        "progress_percent": 100,
                        "ready_to_redeem": true,
                        "stock_label": "In stock",
                        "stock_detail_label": "In stock",
                        "has_variants": true,
                        "variant_types": [
                          "size",
                          "color"
                        ],
                        "digital_delivery": false,
                        "personalizable": false,
                        "wishlisted": false,
                        "can_quick_redeem": false
                      }
                    ],
                    "meta": {
                      "current_page": 1,
                      "per_page": 20,
                      "total_count": 6,
                      "total_pages": 1,
                      "has_next_page": false,
                      "has_prev_page": false
                    }
                  },
                  "unread_notification_count": 3
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token"
          },
          "403": {
            "description": "`insufficient_permissions` — the token lacks `read:company_store` (checked first, before the app gate). `access_denied` — the Company Store app is not accessible to this caller (not enabled for the tenant, or the caller is outside the app's visibility rules). `store_disabled` — the tenant's admin has switched the store off.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreError"
                },
                "examples": {
                  "store_disabled": {
                    "value": {
                      "error": {
                        "code": "store_disabled",
                        "message": "The Company Store is currently disabled"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/company-store/catalog/categories": {
      "get": {
        "tags": [
          "Company Store"
        ],
        "summary": "Catalog filter sheet",
        "description": "The category sheet on its own, so a client can open the filter without\npulling a page of items — the same read split\n`GET /recognitions/leaderboard/categories` makes.\n\nReturns the **same** `filters` block the listing returns, built from the\nsame code, so the two can never disagree about a count. `search` and\n`region_id` are honoured for the reason the web filter rows honour them:\nevery row preserves the active term, so a count computed without it\nwould advertise results the filtered grid cannot produce.\n\nThe shared context blocks (`viewer`, `features`, `region`, `balance`,\n`redemption`, `available_regions`) ride along, which makes this a\nreasonable \"open the store\" first call for a client that wants the\nfilter sheet before the first grid. There is no `items` or `meta`.\n",
        "operationId": "companyStoreCatalogCategories",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Scope the counts to a search term, as the grid's own rows do."
          },
          {
            "name": "region_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Scope the counts to a region."
          }
        ],
        "responses": {
          "200": {
            "description": "The filter sheet",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "catalog": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/CompanyStoreContext"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "filters": {
                              "$ref": "#/components/schemas/CompanyStoreFilters"
                            }
                          }
                        }
                      ]
                    },
                    "unread_notification_count": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token"
          },
          "403": {
            "description": "`insufficient_permissions`, `access_denied` or `store_disabled` — see the listing endpoint."
          }
        }
      }
    },
    "/company-store/catalog/{id}": {
      "get": {
        "tags": [
          "Company Store"
        ],
        "summary": "One catalog item",
        "description": "Everything the item detail screen renders: the card's own fields plus the\nphoto strip, the variant groups, every payment method with its own\nrefusal reason, the web page's Item Details block, the caller's\nwishlist / back-in-stock watch state, and the related-items strip.\n\n### Unavailable items are served on purpose\n\nAn out-of-stock, coming-soon, discontinued or draft item returns **200**,\nexactly as the web detail page renders it — with a status alert instead\nof the buy controls. Read `available`, `status`, `status_label`,\n`stock_label` and `payment_options[].block_reason` and render the same\nway. Hiding the item would break a deep link from an order, a\nnotification or a wishlist.\n\nRegion and audience restrictions DO refuse, because those are disclosure\nboundaries rather than states:\n\n* **audience** → `404 not_found`, with copy that deliberately does not\n  reveal that the item exists or who is in the group. Identical to the\n  answer a genuinely missing id gets.\n* **region** → `403 region_restricted` (or `region_unavailable` when the\n  tenant has regions on and this caller resolves to none). Regions are a\n  merchandising axis, not a secret, so the reason is named and a client\n  can offer the switch.\n\n### `payment_options`\n\nOne entry per payment path the ITEM has a price for — a method the item\ncannot be bought with at all is omitted entirely, because there is no tab\nto render. A method the TENANT has switched off is present with\n`available: false` and a `block_reason`, because that is a state an admin\ncan change and a client should be able to explain it.\n\nThe availability rules are the ones the web checkout builds its payment\noptions from, including the two that matter most: `points` requires the\ncaller's balance to actually cover it (`block_reason: \"Insufficient\npoints\"`), and `mixed` requires the card AND points toggles to both be\non, because a split spends from both.\n\nWhether the user has finished choosing variants is client state, so it is\nreported as `has_variants` / `variants` rather than folded into\n`block_reason` — otherwise every freshly-opened detail screen would look\nbroken.\n",
        "operationId": "showCompanyStoreCatalogItem",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "StoreItem id."
          },
          {
            "name": "region_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "The region to evaluate access in. A region-specific item is reachable only from its own region; global items are reachable from every region."
          }
        ],
        "responses": {
          "200": {
            "description": "The item, with the caller's context",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "catalog": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/CompanyStoreContext"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "item": {
                              "$ref": "#/components/schemas/CompanyStoreItemDetail"
                            }
                          }
                        }
                      ]
                    },
                    "unread_notification_count": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "catalog": {
                    "item": {
                      "id": 1,
                      "name": "Company Logo Hoodie",
                      "description": "Soft fleece pullover hoodie with an embroidered company logo.",
                      "category": "swag",
                      "category_label": "Swag",
                      "category_icon": "tshirt",
                      "category_color": "primary",
                      "image_url": "https://acme.workforce.mangoapps.com/rails/active_storage/blobs/redirect/ey/hoodie.jpg",
                      "large_image_url": "https://acme.workforce.mangoapps.com/rails/active_storage/blobs/redirect/ey/hoodie.jpg",
                      "featured": true,
                      "status": "active",
                      "status_label": "Active",
                      "available": true,
                      "low_stock": false,
                      "inventory_count": null,
                      "stock_label": "In stock",
                      "stock_detail_label": "In stock",
                      "requires_shipping": true,
                      "points_price": 4500,
                      "cash_price": null,
                      "price_label": "4500 pts",
                      "redeemable_with_points": true,
                      "purchasable_with_cash": false,
                      "affordable": true,
                      "points_to_go": 0,
                      "progress_percent": 100,
                      "ready_to_redeem": true,
                      "has_variants": true,
                      "variant_types": [
                        "size",
                        "color"
                      ],
                      "digital_delivery": false,
                      "personalizable": false,
                      "wishlisted": true,
                      "restock_watch": false,
                      "can_quick_redeem": false,
                      "category_enabled": true,
                      "requires_approval": false,
                      "photos": [
                        {
                          "url": "https://acme.workforce.mangoapps.com/rails/active_storage/blobs/redirect/ey/hoodie.jpg",
                          "alt": "Company Logo Hoodie"
                        },
                        {
                          "url": "https://cdn.mangoapps.com/library/hoodie-back.jpg",
                          "alt": "Company Logo Hoodie photo 2"
                        }
                      ],
                      "variants": [
                        {
                          "type": "size",
                          "label": "Size",
                          "required": true,
                          "options": [
                            "S",
                            "M",
                            "L",
                            "XL"
                          ]
                        },
                        {
                          "type": "color",
                          "label": "Color",
                          "required": false,
                          "options": [
                            "Black",
                            "Heather Gray"
                          ]
                        }
                      ],
                      "item_details": {
                        "category": "swag",
                        "category_label": "Swag",
                        "sku": "SWAG-HOOD-01",
                        "provider_type": "internal",
                        "provider_label": "Internal",
                        "requires_shipping": true,
                        "estimated_delivery_days": 5,
                        "delivery_label": "5 business days",
                        "fair_market_value": 45.0
                      },
                      "payment_options": [
                        {
                          "key": "points",
                          "label": "Redeem with Points",
                          "points_required": 4500,
                          "cash_required": null,
                          "available": true,
                          "block_reason": null
                        },
                        {
                          "key": "cash",
                          "label": "Buy with Card",
                          "points_required": null,
                          "cash_required": 45.0,
                          "available": false,
                          "block_reason": "Card purchases aren't enabled for this store"
                        }
                      ],
                      "related_items": []
                    }
                  },
                  "unread_notification_count": 3
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token"
          },
          "403": {
            "description": "`insufficient_permissions` / `access_denied` / `store_disabled` (see the listing endpoint), or `region_restricted` / `region_unavailable` for an item outside the caller's region.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreError"
                },
                "examples": {
                  "region_restricted": {
                    "value": {
                      "error": {
                        "code": "region_restricted",
                        "message": "This item is not available in your region."
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such item in this tenant — or an item restricted to a recipient group this caller is not in. Deliberately the same answer for both, so the response cannot disclose that a targeted item exists.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreError"
                },
                "examples": {
                  "audience_restricted": {
                    "value": {
                      "error": {
                        "code": "not_found",
                        "message": "This item isn't available for redemption right now."
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/company-store/catalog/{id}/checkout": {
      "get": {
        "tags": [
          "Company Store"
        ],
        "summary": "Price a checkout and disclose what will happen",
        "description": "Everything the **Checkout** screen renders before anything is committed —\nthe native mirror of the web checkout GET.\n\n### What it answers\n\n* **`payment_options`** — every path this item *could* take, each with\n  `available` and, when it isn't, a `block_reason` written for the buyer.\n  Built by the same method `GET /catalog/{id}` uses, so the item screen\n  and the checkout screen cannot disagree. Two rules here are\n  load-bearing and have both been bugs: `points` needs the wallet to\n  actually cover it, and `mixed` needs **both** the cash and points\n  toggles on, not just the mixed one.\n* **`totals`** — what each path costs at this `quantity`, priced through\n  `::Store::CheckoutService`'s own public conversions rather than\n  re-derived here.\n* **`shipping.collected_by`** — `app` or `stripe`. This is the one thing a\n  native client cannot work out for itself: with Stripe Tax on, a\n  shippable item's address is collected by **Stripe** so `automatic_tax`\n  can compute destination tax on it, and your form must skip its address\n  step or the buyer types it twice.\n* **`disclosures`** — whether this redemption will be **held for\n  approval** (and by which tier), whether the velocity brake will hold\n  it, and the monthly points cap with what is left of it. For a one-tap\n  redeem this is the only place a hold can be disclosed at all.\n\n### The mixed-payment slider\n\n`totals.mixed.max_points` is the slider's ceiling:\n`min(points needed to cover the whole price, the caller's balance)`.\nSpend past it and there is no card portion left, which the write detects\nand routes to the points path — so a slider allowed past the ceiling\nsilently changes which payment type the buyer gets.\n\nPass `points_to_use` to have the split quoted back:\n`totals.mixed.cash_after_points` is what the card will be charged.\n\n### It does not refuse an unbuyable item\n\nAn out-of-stock, inactive, or switched-off-category item still answers\n**200**, with `can_checkout: false` and a `block_reason` on every path.\nA 403 here would make \"this category is switched off\" indistinguishable\nfrom \"you are not in this item's audience\", and that distinction is\nexactly what must not be disclosed. Region and audience restrictions DO\nrefuse (404/403), because those are disclosure boundaries rather than\nstates.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The numeric store item id.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "quantity",
            "in": "query",
            "required": false,
            "description": "Prices every path at this quantity. Clamped to 1..99, and `quantity.max` reports the real ceiling for this item (its tracked inventory, when it tracks any).",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "payment_type",
            "in": "query",
            "required": false,
            "description": "Which path the buyer picked on the item screen. Echoed back as `selected_payment` so their tap is not lost; ignored (and replaced with the first available path) when this item cannot take it.",
            "schema": {
              "type": "string",
              "enum": [
                "points",
                "cash",
                "mixed"
              ]
            }
          },
          {
            "name": "points_to_use",
            "in": "query",
            "required": false,
            "description": "For `mixed` — quotes the split for this many points without committing anything.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The priced checkout. Nothing has been written.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreCheckoutPreviewResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "* `insufficient_permissions` — the token lacks `read:company_store`\n  (checked first, before the app gate). This preview is a GET; the\n  POST below requires `write:company_store`.\n* `access_denied` — no Company Store access.\n* `store_disabled` — the tenant paused the store.\n* `region_restricted` — the item belongs to another region (or\n  `region_unavailable` when the tenant has regions on and this caller\n  resolves to none). Regions are a merchandising axis, not a secret,\n  so the reason is named and a client can offer the switch.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreCheckoutError"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no such item in this tenant, OR it is restricted to an\naudience group this caller is not in. Deliberately the same answer,\nwith copy that does not reveal that the item exists or who is in the\ngroup. Same contract as GET /company-store/catalog/{id}.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreCheckoutError"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Company Store"
        ],
        "summary": "Place an order — points, cash, or mixed",
        "description": "**The write.** One endpoint, three payment modes, exactly as the web\n`#process_checkout` is one action with three branches.\n\n### Points\n\nPlaced in this request. `placed: true`, and the response carries the\norder and the wallet **after** the debit.\n\n`status` may be `pending_approval`: high-value redemptions and any order\ntripping the velocity brake are HELD, with the points deducted but no\nfulfilment until an approver releases it. **Do not celebrate a hold** —\n`requires_approval` and the `message` both say so, and the design's own\nflow branches on it.\n\n### Cash and mixed\n\nThe order is created **pending**: the points portion is debited and the\nstock is reserved *before* the buyer pays, because that is the only way\nto stop two people spending the same points or buying the last unit.\n`placed: false`, and `payment` carries the Stripe Checkout URL.\n\nOpen `payment.checkout_url` in the system browser. Then either intercept\n`payment.return_url_prefix` / `payment.cancel_url_prefix` on the\nin-browser navigation, or simply poll `payment.complete_url` once the\nbrowser closes — they are the same call.\n\nThe success and cancel URLs are the app's own, and are deliberately\n**not** client-supplied: a caller-controlled `success_url` is an open\nredirect that we would also be handing to Stripe, and the service\nresolves the item's product image against it (a custom app scheme there\nships a session with no images).\n\n### Every toggle is re-enforced here\n\n`payment_type` is user-controlled, so the GET's filtering is a courtesy,\nnot the gate. `points_to_use` is additionally forced to **0** for a\n`cash` checkout — without that, a hidden mixed-payment slider leaking a\nvalue into a card-only submit silently spends points.\n\n### Idempotency\n\nSend `idempotency_key`. A repeat returns the SAME order — and for a card\ncheckout, the same still-open Stripe session — instead of a second\npending order and a second points debit. If the retried session is no\nlonger payable (already paid, or expired), the response reports the order\nalone with no `payment` block rather than handing back a dead URL.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The numeric store item id.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CompanyStoreCheckoutRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Either the order was PLACED (`placed: true`, points path) or a Stripe session is waiting to be paid (`placed: false`, with `payment`). Branch on `placed`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreCheckoutWriteResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "* `insufficient_permissions` — the token lacks `write:company_store`\n  (checked first, before the app gate).\n* `access_denied` — no Company Store access.\n* `store_disabled` — the tenant paused the store.\n* `region_restricted` / `region_unavailable` — the item belongs to\n  another region, or the tenant has regions on and this caller\n  resolves to none.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreCheckoutError"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no such item in this tenant, OR it is restricted to an\naudience group this caller is not in. Deliberately the same answer,\nso a deep link cannot confirm that a targeted item exists.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreCheckoutError"
                }
              }
            }
          },
          "409": {
            "description": "`checkout_session_unavailable` — an idempotent RETRY (same\n`idempotency_key`) whose order is still awaiting payment but whose\nStripe session can no longer be reopened: it expired, it was closed,\nor Stripe could not be reached. Nothing was written and nothing was\npaid; the points are still held and the stock still reserved.\n\n`error.details` carries `order`, `wallet`, and both closing\nendpoints — `complete_url` in case the session WAS paid before it\nclosed, `abandon_url` to hand the points and the stock back now\nrather than waiting for the 25h sweep.\n\nThis is NOT reported as a success: `placed: true` means \"nothing\nfurther is owed\", and an unpaid pending order does not qualify.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreCheckoutError"
                }
              }
            }
          },
          "422": {
            "description": "Nothing was written. Every code here is a refusal of this request\ngiven current state, which is what a client retries with different\ninput.\n\n* `invalid_payment_type` — not one of points / cash / mixed.\n* `points_disabled` — the tenant switched redemption off, or this\n  item carries no points price. Answers a `mixed` submit carrying\n  `points_to_use > 0` as well as a `points` one: a split payment\n  spends points, so it needs the points toggle too — which is why\n  `payment_options[mixed].available` requires all three.\n* `cash_disabled` — the tenant switched card payments off.\n* `mixed_disabled` — the tenant switched split payments off.\n* `item_unavailable` — the item's category is switched off. Note the\n  GET still renders such an item; only the write refuses.\n* `variants_required` — the item has options and the selection is\n  incomplete.\n* `checkout_failed` — the service refused, and `message` is its own\n  prose written for the buyer: not enough points (with the numbers),\n  the team's store budget is exhausted, it just sold out, the\n  monthly cap would be crossed, engraving details are missing, or\n  Stripe rejected the session. Deliberately one code — the service\n  does not return machine-readable reasons, and matching on its\n  strings here would break the moment the copy is edited. Show\n  `message` to the buyer.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreCheckoutError"
                }
              }
            }
          }
        }
      }
    },
    "/company-store/checkout/{order_number}/complete": {
      "post": {
        "tags": [
          "Company Store"
        ],
        "summary": "Confirm the card leg of a checkout (idempotent — poll this)",
        "description": "Closes the round trip: verifies with Stripe that the session was paid,\ncaptures the payment intent, flips the order to `processing`, copies back\nany address Stripe collected, fulfils digital items inline, enqueues\nprovider fulfilment, and sends the buyer's confirmation **once**.\n\n### This is a polling endpoint\n\nIt is idempotent by design, because a native return trip may never\narrive. All of these are the same call:\n\n* the client intercepted the success URL — confirm now;\n* the browser closed and the client doesn't know what happened — ask;\n* the app is reconciling on next launch, an hour later — ask again.\n\nAn order already `processing`/`fulfilled` answers **200** with the order\nand does **not** re-send the confirmation email or the admin alert (the\nservice's own first-capture guard). That 200 holds even when Stripe\ncannot be re-reached to re-verify a capture that already happened —\n`processing` is only ever written by the capture itself, so the status is\nthe proof, and a polling client must not be handed `checkout_closed`\n(whose contract is \"terminal, stop polling\") for a paid order that is\nbeing prepared.\n\n### Why a webhook is not doing this\n\nThere is no `checkout.session.completed` handling for store sessions —\nsee the file header. This endpoint and the 6-hourly\n`CompanyStore::StaleCheckoutSweepJob` are the only two paths that will\never confirm a store payment, which is why a client should poll rather\nthan assume the redirect landed.\n\n### Own checkouts only\n\nCompleting a payment is the buyer closing their own round trip; there is\nno surface anywhere, web included, where one person finishes another's.\nSomebody else's order is **404**, not 403 — whether that order number\nexists is not this caller's business.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/CompanyStoreCheckoutOrderNumber"
          }
        ],
        "responses": {
          "200": {
            "description": "Paid and confirmed — or already was. `placed: true` and the order is `processing` or beyond.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreCheckoutCompleteResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "`insufficient_permissions` (the token lacks `write:company_store`) / `access_denied` / `store_disabled`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreCheckoutError"
                }
              }
            }
          },
          "404": {
            "description": "No such order number among this caller's own orders in this tenant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreCheckoutError"
                }
              }
            }
          },
          "409": {
            "description": "Nothing changed. `error.details.order` carries the order's current\nstate so a polling client can decide what to do without a second\nrequest.\n\n* `payment_incomplete` — the order is still `pending` and Stripe says\n  the session is not paid. The buyer hasn't finished. **Keep\n  polling** (or stop and let the sweep reconcile).\n* `checkout_closed` — the order is terminal (cancelled, refunded, or\n  swept). **Stop polling and refresh.** The service refuses to\n  resurrect a terminal order, and if that session was in fact paid in\n  the race window it refunds the buyer rather than re-fulfilling.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreCheckoutError"
                }
              }
            }
          },
          "422": {
            "description": "`no_checkout_session` — this order was not paid by card (a points-only redemption), so there is no payment to confirm.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreCheckoutError"
                }
              }
            }
          }
        }
      }
    },
    "/company-store/checkout/{order_number}/abandon": {
      "post": {
        "tags": [
          "Company Store"
        ],
        "summary": "Abandon an unpaid checkout and hand the points back now",
        "description": "The native equivalent of the web's Stripe cancel landing. Cancels the\npending order and immediately restores the points, the team budget\ndraw-down and the reserved stock, then expires the Stripe session so it\ncan no longer be paid.\n\n**Why this endpoint has to exist:** a cash/mixed order is created before\npayment, so a buyer who backs out of the browser leaves points debited\nand stock reserved. The web flow learns about it from Stripe's own cancel\nlink. A native client has no such link — it has to say so, and without\nthis call those points stay held until\n`CompanyStore::StaleCheckoutSweepJob` runs, up to ~31 hours later.\n\nCall it when the buyer dismisses the payment browser without paying.\n\nOnly a **pending** order can be abandoned. A paid one needs a\ncancellation (which carries a card refund) — that is\n`POST /company-store/orders/{order_number}/cancel`, and the 409 here\npoints at it.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/CompanyStoreCheckoutOrderNumber"
          }
        ],
        "responses": {
          "200": {
            "description": "Cancelled. Points, budget and stock restored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreCheckoutAbandonResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "`insufficient_permissions` (the token lacks `write:company_store`) / `access_denied` / `store_disabled`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreCheckoutError"
                }
              }
            }
          },
          "404": {
            "description": "No such order number among this caller's own orders in this tenant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreCheckoutError"
                }
              }
            }
          },
          "409": {
            "description": "* `not_abandonable` — the order is no longer pending. If it was paid,\n  cancel it instead; `error.details.cancel_url` is the endpoint to\n  use.\n* `checkout_closed` — it moved out from under the request (paid or\n  swept in that instant). Refresh the order.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreCheckoutError"
                }
              }
            }
          },
          "422": {
            "description": "`no_checkout_session` — a points-only order has no checkout to abandon. Cancel it instead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreCheckoutError"
                }
              }
            }
          }
        }
      }
    },
    "/company-store/shipping-countries": {
      "get": {
        "tags": [
          "Company Store"
        ],
        "summary": "Shipping countries and their states/provinces",
        "description": "Everything needed to render the checkout's country and state dropdowns:\nthe shippable countries, each country's states nested underneath it, and\nthe values the checkout write accepts for both fields.\n\nTakes no parameters.\n\n### Read this before you submit an address\n\nRender `label`; submit `value`. The two fields' value shapes deliberately\ndiffer — country submits `\"United States\"` (the display name) while state\nsubmits `\"CA\"` (the 2-letter code). Sending an ISO country code or a\nspelled-out state name is accepted by the checkout and then fails at\nfulfillment, after the points are spent. See this file's header.\n\n### Why states are nested rather than a separate lookup\n\nNested under their country, so there is no correlation key to get wrong\nand no second request to make. The whole payload is ~52 static rows. The\ncascading per-country fetch pattern used elsewhere in the product\n(`/api/jurisdictions`) exists for the tenant-seeded jurisdiction tree,\nwhich is thousands of cities deep; this is not that.\n\n### When a country has no state list\n\n`states` is `[]` and `prefill.states_required` is `false`. That pair means\n**render a free-text field**, not \"still loading\" — a client that showed\nan empty dropdown would leave the buyer unable to complete the address.\nToday every offered country has a list, so this is forward-compatibility,\nnot a live case.\n\n### `prefill`\n\nWhat the web form pre-fills from the caller's own profile, resolved the\nsame way. `state` is normalised to a code because a stored profile state\nis not one shape in practice — an imported address may hold\n`\"California\"`, `\"CA\"` or `\"ca\"`. A profile state that resolves to no\nknown code comes back **null** rather than a guess: that is exactly the\ncase where the buyer must choose, and pre-selecting a wrong state is\nworse than pre-selecting none.\n\nNever `403`/`404` for a caller past the gates, and never empty — a\nsuccessful response always carries at least one country.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The shippable countries, their states, and the caller's prefill.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreShippingCountriesResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token"
          },
          "403": {
            "description": "`insufficient_permissions` (the token holds neither `read:company_store` nor `read:own_company_store` — checked first, before the app gate), `access_denied` (the app isn't accessible to this caller) or `store_disabled` (an admin paused the store).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreShippingCountriesError"
                }
              }
            }
          }
        }
      }
    },
    "/company-store/orders": {
      "get": {
        "tags": [
          "Company Store"
        ],
        "summary": "Order history with per-status counts",
        "description": "Everything the **Orders** tab renders in one request: the paginated order\nlist, the filter pill row with a **count per status**, the caller's\nrole/tenant context, and the page envelope.\n\n### Whose orders\n\n`scope` decides the pool, and it is never widened implicitly:\n\n* **`mine` (default)** — the caller's own orders. This is what an\n  employee, a manager and an admin all get when they open the tab, so an\n  admin's Orders screen is their own order history exactly like everyone\n  else's.\n* **`all`** — every order in the tenant, the read-side of the web admin\n  orders queue. Each row additionally carries `employee`. Available only\n  to a **store admin** (a business admin/owner or a Company Store\n  app-admin); anyone else gets **403 `forbidden`** rather than a silently\n  narrowed list, which would answer a different question than the one\n  asked. `can_view_all_orders` tells a client whether to offer the toggle\n  at all.\n\nThe mobile design has two personas — Employee and Manager\n(\"everything above, plus approves orders\") — and the Orders screen is\n**identical for both**. A manager's extra is the separate redemption\napproval queue, not a wider order list, so nothing on this endpoint keys\noff manager status.\n\n### Counts and the pill row\n\n`counts` carries **every** `StoreOrder` status plus `all`, always present\n(0 when empty), from ONE grouped query. `status_filters` is that same\ndata arranged as the design's pill row — value, label, count, whether it\nis selected, and whether the web chip would show it.\n\nTwo rules matter and are easy to get wrong client-side:\n\n* Counts honour **`search`** but NOT **`status`**. Each pill has to report\n  how many rows tapping it would land on, so narrowing the counts by the\n  currently-selected status would make every other pill read 0. A pill\n  that ignored the search term instead would over-count — advertising\n  \"Fulfilled 12\" onto a list of 2.\n* `visible` is false for `pending_approval` and `refunded` while their\n  count is 0 and they are not selected — these are rare states and the web\n  chips hide them rather than showing a permanent \"Refunded 0\". The other\n  five are always visible. A client can ignore `visible` and render all\n  seven; it exists so the pill row can match the web without hardcoding\n  the vocabulary.\n\n`pending_approval` is labelled **\"Awaiting Approval\"**, not a titleized\n`Pending Approval`, because it would otherwise collide with the separate\n`pending` status on the same screen. Every store surface says the same.\n\nNote that **`pending` and `pending_approval` are counted separately and\nboth appear in the list**, matching the shipped web page rather than the\nmobile design's prototype (which folded held orders into the Pending pill\nwhile hiding them from the list — so a held order was counted but\nunreachable). A held redemption has the employee's points locked up; it is\nthe order they are most likely to be looking for, so it is listed, given\nits own pill, and carries an `approval` block on the detail payload. A\nclient that wants the prototype's grouping can add the two counts.\n\n### Gotchas\n\n* An unrecognised `status` is **ignored** (the response falls back to\n  All) rather than returning nothing — `filters.status` reports what was\n  actually applied, so a client can tell the difference.\n* `per_page` is clamped server-side to 50; `filters.per_page` and\n  `meta.per_page` report the value in force.\n* `has_fulfillment_error` is true only while the order is still `pending`.\n  A manually resolved order stops advertising the failure that preceded\n  it, so a client must not treat a resolved order as failed.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Narrow to one status. Blank/absent = All. An unrecognised value is IGNORED (falls back to All) rather than returning an empty list.",
            "schema": {
              "type": "string",
              "enum": [
                "pending_approval",
                "pending",
                "processing",
                "fulfilled",
                "cancelled",
                "refunded"
              ]
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Case-insensitive match on the order number or the ordered item's name — the two things a user remembers about an old order. In the `all` scope it additionally matches the employee's name and email. Narrows the list AND the counts together.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "description": "`mine` (default) or `all`. `all` is the whole tenant's orders and requires a store admin — 403 `forbidden` otherwise. Any other value is treated as `mine`.",
            "schema": {
              "type": "string",
              "enum": [
                "mine",
                "all"
              ],
              "default": "mine"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based page number. Values below 1 are treated as 1.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Rows per page. Clamped to 50; a non-positive or junk value falls back to 20.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's order history, its filter bar and its counts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "orders": {
                      "type": "object",
                      "required": [
                        "viewer",
                        "features",
                        "scope",
                        "can_view_all_orders",
                        "filters",
                        "counts",
                        "status_filters",
                        "items",
                        "meta"
                      ],
                      "properties": {
                        "viewer": {
                          "$ref": "#/components/schemas/CompanyStoreOrdersViewer"
                        },
                        "features": {
                          "$ref": "#/components/schemas/CompanyStoreOrdersFeatures"
                        },
                        "scope": {
                          "type": "string",
                          "enum": [
                            "mine",
                            "all"
                          ],
                          "description": "The pool this page was actually drawn from."
                        },
                        "can_view_all_orders": {
                          "type": "boolean",
                          "description": "Whether this caller may request `scope=all`. Render the scope toggle from this rather than from a role guess, so a client never offers a switch whose request would 403."
                        },
                        "filters": {
                          "type": "object",
                          "description": "The filter values the server actually APPLIED (not what was sent).",
                          "properties": {
                            "status": {
                              "type": "string",
                              "nullable": true,
                              "description": "null when no status filter is in force (including when an unknown one was dropped)."
                            },
                            "search": {
                              "type": "string",
                              "nullable": true
                            },
                            "per_page": {
                              "type": "integer",
                              "description": "The clamped page size in force."
                            }
                          }
                        },
                        "counts": {
                          "$ref": "#/components/schemas/CompanyStoreOrderStatusCounts"
                        },
                        "status_filters": {
                          "type": "array",
                          "description": "The design's pill row, in its display order — All, Awaiting Approval, Pending, Processing, Fulfilled, Cancelled, Refunded. The numbers are the same ones `counts` reports.",
                          "items": {
                            "$ref": "#/components/schemas/CompanyStoreOrderStatusFilter"
                          }
                        },
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/CompanyStoreOrderListRow"
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/CompanyStoreOrdersPageMeta"
                        }
                      }
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "Native app badge count (the shared api/v1 envelope)."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "One of four refusals, distinguished by `error.code`:\n\n* `insufficient_permissions` — the token lacks `read:company_store`.\n  Checked first, before any of the three below.\n* `access_denied` — the Company Store app isn't enabled for this\n  tenant, or this user is outside the app's audience.\n* `store_disabled` — the tenant's admin has paused the store.\n* `forbidden` — `scope=all` was requested by someone who isn't a\n  store admin.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "access_denied",
                            "store_disabled",
                            "forbidden"
                          ]
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/company-store/orders/{order_number}": {
      "get": {
        "tags": [
          "Company Store"
        ],
        "summary": "One order in full — timeline, reward payload and available actions",
        "description": "The **Order Detail** screen. Built on the same shared row card the list\nuses, so the two can never disagree about status, money or the item, plus\neverything the detail screen adds.\n\n### The timeline\n\n`timeline` comes from `Store::OrderTimeline`, shared **verbatim** with the\nweb page's own Order Timeline card. It is **derived** from the order's\nstatus, its lifecycle timestamps and its item's delivery shape — which is\nwhy it can name what is still to come (\"On its way · Est. 5 business days\nafter it ships\"), the thing an employee actually opens this screen for.\nThat is deliberately different from the append-only audit trail the web\n*admin* order page renders.\n\nSteps are ordered and each carries a `state`:\n\n| state | meaning |\n|---|---|\n| `done` | already happened |\n| `current` | the step the order is standing on |\n| `upcoming` | not reached yet — still named, so the user can see what's next |\n| `cancelled` | terminal: cancelled |\n| `refunded` | terminal: refunded |\n\nA live order runs `placed → [awaiting_approval] → processing → delivered`;\na cancelled or refunded one stops at `placed → cancelled|refunded` and\noffers nothing forward-looking, because there is nowhere left to go.\n\n### What each role gets\n\n* **The person who placed it** — everything below, including the\n  `fulfillment` reward payload (tracking number and link, gift card code,\n  redemption link, delivery email, donation receipt) and the shipping\n  address. Exactly what the web order page shows the same person.\n* **A store admin** — may open **anyone's** order (`is_mine: false`), and\n  additionally receives the `admin` block: provider order id, provider\n  status, funding source and the raw connector `fulfillment_error`. The\n  web shows these on the admin order page only; an employee gets\n  reassurance copy, never a connector error string.\n* **Anyone else** — 403 `forbidden` on someone else's order.\n\n### Actions are affordances, not permissions to guess at\n\n`actions` is resolved server-side against the very predicates the write\npaths enforce — including this API's own\n`POST :order_number/cancel` — so a control rendered from it is one whose\nwrite would be accepted, and one the server accepts is never hidden.\n`cancel_blocked_reason` explains a disabled cancel button while the order\nstill *looks* cancellable (already dispatched to the reward provider, for\ninstance) instead of letting the tap bounce.\n\n`can_cancel` is offered to **both** people the web offers it to: the buyer\n(self-service) and a store admin on anyone's order, with `cancel_as`\nnaming which. The other three — return, report a problem, reorder — are\nthe **buyer's own** acts and are `false` for an admin however wide their\nread access, exactly as on the web.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "order_number",
            "in": "path",
            "required": true,
            "description": "The ORDER NUMBER (e.g. `ORD-20260819-ABC123`), the same identifier the web URLs use — not the numeric id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The order, its timeline, its reward payload and the viewer's available actions.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "order": {
                      "$ref": "#/components/schemas/CompanyStoreOrderDetail"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "Native app badge count (the shared api/v1 envelope)."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "`insufficient_permissions` (the token lacks `read:company_store`,\nchecked first), `access_denied` (no Company Store access),\n`store_disabled` (the tenant paused the store), or `forbidden` — the\norder belongs to someone else and the caller is not a store admin.\nDeliberately distinct from 404: the order exists in this tenant, the\ncaller just may not read it.\n"
          },
          "404": {
            "description": "No order with that number in this tenant."
          }
        }
      }
    },
    "/company-store/orders/{order_number}/cancel": {
      "post": {
        "tags": [
          "Company Store"
        ],
        "summary": "Cancel an order",
        "description": "Cancels the order and reverses everything the purchase did — the native\ntwin of **both** web cancel buttons.\n\n### Who may cancel\n\nResolved server-side by `StoreOrder#cancel_actor_for`, the same rule the\ndetail payload's `actions.can_cancel` is built from, so a control rendered\nfrom that payload is one this endpoint accepts:\n\n| `actor` | Who | Reach |\n|---|---|---|\n| `owner` | the employee who placed the order | while nothing irreversible has happened — not fulfilled, and not yet dispatched to the reward provider. Mirrors the web order page's own **Cancel order** button. |\n| `admin` | a **store admin** (business admin/owner, or a Company Store app-admin) on anyone's order | the broader power, reaching an order already at the provider. Mirrors the web **admin** order page's Cancel. Cash orders additionally require a business administrator — see below. |\n\n`owner` wins when a store admin cancels their own cancellable order: the\nnarrower claim, and the one the employee-facing note records.\n\nEverything else the buyer can do (return, report a problem, reorder) stays\nthe buyer's alone — an admin is offered none of them on somebody else's\norder.\n\n### Cash orders move real money\n\nCancelling a cash or mixed order enqueues a **live Stripe refund**\n(`CompanyStore::PaymentRefundJob`), so it requires a **business\nadministrator** — a Company Store app-admin is not enough and gets 403\n`cash_reversal_forbidden`, a distinct code because nothing is wrong with\nthe request: a different person has to take the action. The buyer's own\nself-service cancel is unaffected — that is the same money going back to\nthe same person.\n\n### What the cancel does\n\nThe work is `StoreOrder#cancel!` — one row-locked transaction, unchanged\nand unwrapped, exactly as both web buttons invoke it:\n\n* refunds the points to the employee's wallet, and to the team store\n  budget the redemption was drawn from, if any\n* restocks the item's inventory by the order quantity\n* reverses a captured card charge (async — `card_refund_pending` says when\n  one is on its way) and expires a still-open Stripe Checkout Session, so\n  a cancelled order can't be paid for afterwards\n* clears the stale in-flight fulfilment flags, so a cancelled order stops\n  reporting the failure that preceded it\n* notifies the employee (email + in-app) off the status change\n\nNo second notification and no second write are added by this endpoint.\n\n### The response\n\nCarries the **full re-rendered order detail** alongside the cancellation\nreceipt, so the client updates the screen it just acted on from this one\nresponse instead of following it with a `GET`. The receipt's\n`points_refunded` / `card_refund_pending` are the **pre-write** readings —\nafter the cancel the points are already back and the refund already\nenqueued, so asking the row afterwards would report `0` / `false` and the\nconfirmation would quietly stop naming the money.\n\nIdempotency: a second cancel of the same order answers 409\n`not_cancellable`. Nothing is refunded twice.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "order_number",
            "in": "path",
            "required": true,
            "description": "The ORDER NUMBER (e.g. `ORD-20260819-ABC123`), the same identifier the web URLs and the detail endpoint use — not the numeric id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "description": "Why it was cancelled. Appended to the order's own audit notes, which is where both web pages record it and where an admin reads it back. Optional; whitespace-only is treated as none given. NOT quoted in the employee's cancellation notification (neither web path quotes one) — it is an audit note, not a message.",
                    "example": "Ordered the wrong size"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cancelled. Points refunded, inventory restocked, employee notified.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreOrderCancelResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "* `insufficient_permissions` — the token lacks `write:company_store`\n  (checked first, before any of the below).\n* `access_denied` — no Company Store access.\n* `store_disabled` — the tenant paused the store.\n* `forbidden` — the order is somebody else's and the caller is not a\n  store admin. Retrying will never help; a client should stop\n  offering the control.\n* `cash_reversal_forbidden` — a store admin who is not a **business**\n  administrator, on a cash/mixed order. The order IS cancellable; a\n  different person has to do it.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreOrderCancelError"
                }
              }
            }
          },
          "404": {
            "description": "No order with that number in this tenant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreOrderCancelError"
                }
              }
            }
          },
          "409": {
            "description": "* `not_cancellable` — it IS this caller's to cancel, but the order has\n  moved on: already fulfilled, already cancelled or refunded, or (for\n  the buyer's own self-service cancel) already dispatched to the\n  reward provider. The caller's screen is stale — refresh it. This is\n  deliberately **not** a 403: it is a state answer, not a permissions\n  one, and a client that hid the control here would hide it for orders\n  that are still cancellable.\n* `cancel_failed` — the order moved out from under the request between\n  the permission check and the write (cancelled or fulfilled by\n  someone else in that window). Nothing was changed; refresh and\n  re-read the order.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreOrderCancelError"
                }
              }
            }
          }
        }
      }
    },
    "/company-store/cart": {
      "get": {
        "tags": [
          "Company Store"
        ],
        "summary": "The caller's cart",
        "description": "Every line re-priced from the live item (prices are never stored),\neach with an `issue` when it can't check out as it stands (out of\nstock, region, variant no longer offered …), the totals, the caps a\nstepper needs, the cart's fulfilment route and whether it ships, and\n`blockers` — the reasons a points checkout would be refused, in the\nwords the web cart renders beside its disabled Checkout button.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The cart (empty carts answer 200 with no lines).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreCartResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "`insufficient_permissions`, `access_denied` or `store_disabled`."
          }
        }
      },
      "delete": {
        "tags": [
          "Company Store"
        ],
        "summary": "Empty the cart",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The now-empty cart.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreCartResponse"
                }
              }
            }
          }
        }
      }
    },
    "/company-store/cart/lines": {
      "post": {
        "tags": [
          "Company Store"
        ],
        "summary": "Add an item to the cart",
        "description": "Adds `quantity` of `item_id` with the given `variants`. The same item\nwith the same selection MERGES into its existing line (quantity is\nclamped to 5 and to stock); a different selection is a new line. A\nsixth distinct line is refused `cart_full`.\n\nRefusal codes (all 422, message written for the buyer): `unavailable`,\n`ineligible` (gift card / donation / engraved / cash-only / switched-off\ncategory), `route_mismatch`, `currency_mismatch`, `cart_full`,\n`out_of_stock`, `variant_invalid`, `region`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "item_id"
                ],
                "properties": {
                  "item_id": {
                    "type": "integer"
                  },
                  "quantity": {
                    "type": "integer",
                    "default": 1,
                    "minimum": 1,
                    "maximum": 5
                  },
                  "variants": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "The picked options, keyed by variant type (`{ \"size\": \"L\", \"color\": \"Navy\" }`). Required options must be present."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Added (or merged). `added_line_id` names the line; `warnings` lists anything clamped.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreCartResponse"
                }
              }
            }
          },
          "422": {
            "description": "Refused — see the codes above.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreCartError"
                }
              }
            }
          }
        }
      }
    },
    "/company-store/cart/lines/{id}": {
      "patch": {
        "tags": [
          "Company Store"
        ],
        "summary": "Set a line's quantity",
        "description": "`quantity` 1–5 (clamped to stock); 0 or less removes the line.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The cart line id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "quantity"
                ],
                "properties": {
                  "quantity": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The cart after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreCartResponse"
                }
              }
            }
          },
          "404": {
            "description": "`line_not_found` — not a line of this caller's cart."
          },
          "422": {
            "description": "Refused (e.g. `out_of_stock`)."
          }
        }
      },
      "delete": {
        "tags": [
          "Company Store"
        ],
        "summary": "Remove a line",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The cart after the removal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreCartResponse"
                }
              }
            }
          },
          "404": {
            "description": "`line_not_found`."
          }
        }
      }
    },
    "/company-store/cart/checkout": {
      "get": {
        "tags": [
          "Company Store"
        ],
        "summary": "Preview the cart checkout",
        "description": "Everything the checkout screen renders and nothing re-derived here\nthat the write derives differently: the payment paths this order may\ntake (`points`, `cash`, `mixed`, each with `available` and a\n`block_reason` when the tenant switched it off or the wallet falls\nshort), the one preselected (`points`, or `mixed` when points fall\nshort and a split is offered), the split slider's ceiling\n(`amount.points_max`), the shipping prefill and whether Stripe collects\nthe address for a given payment type (`shipping.collected_on_stripe_for`),\nand the hold / cap disclosures on the points total.\n\nA cart with a blocked line answers 422 `cart_blocked` naming the lines;\nan empty cart 422 `cart_empty`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "payment_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "points",
                "cash",
                "mixed"
              ]
            },
            "description": "Preselect a path (ignored when it isn't available)."
          }
        ],
        "responses": {
          "200": {
            "description": "The preview.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreCartCheckoutPreview"
                }
              }
            }
          },
          "422": {
            "description": "`cart_empty` or `cart_blocked` (details.issues names the lines)."
          }
        }
      },
      "post": {
        "tags": [
          "Company Store"
        ],
        "summary": "Place the cart as one order",
        "description": "ONE `StoreOrder` with one line per cart line, priced per selection,\ndebiting the points total once. `points` places it in this request\n(`placed: true`; `held_for_approval` when a hold applies). `cash` and\n`mixed` create the order PENDING and answer `placed: false` with a\nStripe Checkout URL — open it in the system browser, then poll\nPOST /checkout/{order_number}/complete; POST /checkout/{order_number}/abandon\ncancels it AND restores the lines to the cart. A `mixed` checkout whose\npoints cover the whole price is routed to the points path — branch on\n`placed`, never on what you asked for.\n\nThe cart is emptied on success. Every rule the single-item checkout\napplies (caps, team budgets, approval tiers, velocity brake, inventory\nlocks, idempotency) applies to the cart total.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "payment_type"
                ],
                "properties": {
                  "payment_type": {
                    "type": "string",
                    "enum": [
                      "points",
                      "cash",
                      "mixed"
                    ]
                  },
                  "points_to_use": {
                    "type": "integer",
                    "description": "`mixed` only — the points portion, up to `amount.points_max`."
                  },
                  "shipping_address": {
                    "type": "object",
                    "description": "Required when `shipping.required` and Stripe does not collect it for this payment type.",
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "street1": {
                        "type": "string"
                      },
                      "street2": {
                        "type": "string"
                      },
                      "city": {
                        "type": "string"
                      },
                      "state": {
                        "type": "string",
                        "description": "2-letter code"
                      },
                      "zip": {
                        "type": "string"
                      },
                      "country": {
                        "type": "string"
                      },
                      "phone": {
                        "type": "string"
                      }
                    }
                  },
                  "idempotency_key": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Per-submission token; a retry with the same key returns the same order instead of placing a second."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Placed (`placed: true`) or awaiting card payment (`placed: false` + `payment`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreCartCheckoutResponse"
                }
              }
            }
          },
          "422": {
            "description": "`cart_empty`, `payment_unavailable`, `shipping_incomplete`, or `checkout_failed` with the service's own sentence."
          }
        }
      }
    },
    "/company-store/orders/{order_number}/reorder": {
      "post": {
        "tags": [
          "Company Store"
        ],
        "summary": "Order Again — every line of a finished order back into the cart",
        "description": "The caller's OWN fulfilled or cancelled order, refilled into the cart\nthrough the one cart brain, so every add rule applies: a line that can\nno longer be added (discontinued, out of stock, switched-off category,\nwrong route for what the cart already holds) is named in `skipped`,\nnever dropped silently. The order detail's `actions.reorder_url`\npoints here when `reorder_via` is `cart`; a lone gift card or donation\n(`reorder_via: checkout`) still reorders through its single-item\ncheckout deep link.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "order_number",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The ORDER NUMBER (`ORD-…`)."
          }
        ],
        "responses": {
          "200": {
            "description": "Lines added; the cart summary to render from.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "added": {
                      "type": "integer"
                    },
                    "skipped": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string"
                          },
                          "reason": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "cart": {
                      "type": "object",
                      "properties": {
                        "line_count": {
                          "type": "integer"
                        },
                        "units": {
                          "type": "integer"
                        },
                        "points": {
                          "type": "integer"
                        }
                      }
                    },
                    "cart_url": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`forbidden` — not the caller's own order."
          },
          "409": {
            "description": "`not_reorderable` — the order is still in progress."
          },
          "422": {
            "description": "`nothing_added` — none of the lines can be ordered right now (the reasons are in the message)."
          }
        }
      }
    },
    "/company-store/orders/{order_number}/requests": {
      "post": {
        "tags": [
          "Company Store"
        ],
        "summary": "Message the store admins about an order",
        "description": "Opens a `StoreOrderRequest` on the caller's OWN order — the native\ntwin of the order page's request form. `kind` must be one of the\nkinds the order currently accepts from its owner, which the detail\npayload names in `actions.request_kinds` (`question` on any live\norder; `cancellation` once the order is with the provider and can no\nlonger be self-cancelled — `actions.can_request_cancellation`;\n`delivery_status` while the order is in progress; `address_change`\nbefore dispatch on a shipped order; `change_selection` before dispatch\non an item with options; `code_issue` once a gift card is issued;\n`payment` and `other` on any live order; `damaged` once a\nphysical order is fulfilled). A kind the order doesn't accept is\nrefused 422 with the model's own sentence; one open cancellation\nrequest per order.\n\nThe store admins are told (Inbox + email). A cancellation request\nlands in their queue; approving it recalls the order at the provider,\ncancels it and refunds the points, and the order's own cancellation\nnotification tells the employee. Declines and answers reach the\nemployee in their Inbox and by email, and appear on the order detail's\n`requests[]` with `resolution_notes`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "order_number",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The ORDER NUMBER (`ORD-…`)."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "kind",
                  "message"
                ],
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "question",
                      "cancellation",
                      "delivery_status",
                      "address_change",
                      "change_selection",
                      "damaged",
                      "code_issue",
                      "payment",
                      "other"
                    ]
                  },
                  "message": {
                    "type": "string",
                    "maxLength": 2000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Opened. Carries the request, the re-rendered order card and a message for the buyer.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "request": {
                      "$ref": "#/components/schemas/CompanyStoreOrderRequest"
                    },
                    "order": {
                      "type": "object",
                      "description": "The order card",
                      "as GET /orders renders a row.": null
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not one of the caller's orders (whether it exists is not this caller's business)."
          },
          "422": {
            "description": "`request_invalid` — the kind isn't available for this order, the message is blank, or a cancellation is already requested."
          }
        }
      }
    },
    "/company-store/admin/order-requests/{id}/approve": {
      "post": {
        "tags": [
          "Company Store"
        ],
        "summary": "Admin: approve a cancellation request",
        "description": "Cancellation requests only. Recalls the order at its provider FIRST\n(`::Store::ProviderCancellation` — the admin cancel button's own step),\nthen cancels it locally: points refunded, every line restocked, the\ncard charge reversed, the employee notified off the status change. The\nprovider's outcome is reported, never swallowed: `provider_cancellation`\nis `{ success: true }`, `{ success: false, error }` when the provider\nrefused (the order is still cancelled here — the admin decided knowing\nit may already be in production), or `{ unconfirmed: true, error }` when\nwe cannot tell what the provider did. Cash orders require a **business**\nadministrator (403 `forbidden`).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Approved and cancelled.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "request": {
                      "$ref": "#/components/schemas/CompanyStoreOrderRequest"
                    },
                    "order": {
                      "type": "object"
                    },
                    "provider_cancellation": {
                      "type": "object",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`forbidden` — not a store admin, the token's scope doesn't permit it, or a cash order without a business administrator."
          },
          "409": {
            "description": "`resolved` (already decided), `not_cancellable` or `cancel_failed` (the order moved on)."
          },
          "422": {
            "description": "`invalid` — not a cancellation request."
          }
        }
      }
    },
    "/company-store/admin/order-requests/{id}/decline": {
      "post": {
        "tags": [
          "Company Store"
        ],
        "summary": "Admin: decline a request",
        "description": "The employee sees `reason` in their Inbox and by email. The order is unchanged — nothing is refunded.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Declined.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "request": {
                      "$ref": "#/components/schemas/CompanyStoreOrderRequest"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`forbidden`."
          },
          "409": {
            "description": "`resolved` — already decided."
          }
        }
      }
    },
    "/company-store/admin/order-requests/{id}/answer": {
      "post": {
        "tags": [
          "Company Store"
        ],
        "summary": "Admin: reply to a request",
        "description": "For questions, address changes and damage reports. `reply` is required and reaches the employee in their Inbox and by email.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "reply"
                ],
                "properties": {
                  "reply": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Answered.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "request": {
                      "$ref": "#/components/schemas/CompanyStoreOrderRequest"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`forbidden`."
          },
          "409": {
            "description": "`resolved` — already decided."
          },
          "422": {
            "description": "`invalid` — empty reply."
          }
        }
      }
    },
    "/company-store/points": {
      "get": {
        "tags": [
          "Company Store"
        ],
        "summary": "Points balance, activity roll-up and transaction history",
        "description": "Everything the **Points** tab renders in one request: the wallet hero\nwith its expiry nudge, the 90-day activity roll-up, the 6-month\nearned-vs-spent trend, the all-time breakdown arranged as a filter row\nwith a **count per type**, the paginated transaction history, and the\nself-service tax-statement link.\n\n### Whose points\n\n**The caller's own wallet, for every role.** There is no persona branch\nhere and none on the web page either — an employee, a manager and a store\nadmin all see their own points and nobody else's. Reading another\nemployee's balance is the separate admin balances surface, which has its\nown gate and is not part of this namespace. `viewer` is reported so a\nclient can offer those surfaces, but it changes nothing in this payload.\n\n### The transaction history and its filter\n\n`type` narrows the list to one kind of ledger entry, using the same\nvalidated vocabulary the desktop Transaction Breakdown links and the\n`/m/` pill row use:\n\n| value | what it is | sign |\n|---|---|---|\n| *(absent)* | everything — the **All** pill | either |\n| `credit` | points earned (recognition, award, released pending points) | positive |\n| `debit` | points redeemed at checkout | negative |\n| `adjustment` | an admin correction, or points clawed back with a deleted recognition | **either** |\n| `expiry` | points aged out by the expiry policy (breakage) | negative |\n\n`adjustment` being signed **either way** is the one to get right\nclient-side: an admin adding points and an admin taking them back are both\nadjustments, which is exactly why every row carries `positive` and the web\npage colours the amount on the sign rather than on the type.\n\n### Counts and the pill row\n\n`counts` carries **every** type plus `all`, always present (0 when empty),\nfrom ONE grouped query. `type_filters` is that same data arranged as the\npill row — value, label, count, whether it is selected, and whether the\nweb would show it.\n\nTwo rules matter:\n\n* Counts are **all-time** and are **not** narrowed by `type`. Each pill\n  has to report how many rows tapping it would land on, so narrowing by\n  the active type would make every other pill read 0. This is the same\n  rule the Orders endpoint's status pills follow. Note this means\n  `counts.all` is the whole history even when the list is filtered —\n  `meta.total_count` is the count of the **filtered** list.\n* `visible` is false for `expiry` while its count is 0 and it is not\n  selected. A tenant with expiry switched off never has a single such row,\n  and a permanent \"Expiry 0\" pill is noise; the other four are always\n  visible. A client may ignore `visible` and render all five — it exists\n  so the pill row can match the web without hardcoding the vocabulary.\n\n### Windows are reported, never assumed\n\n`activity.period_days` is **90** — the window the points *screen* shows,\nwhich is deliberately **not** the dashboard widget's 30 days. Both\nsurfaces report the window they computed so a client's header can't claim\none the server didn't.\n\n`trend.months` always holds 6 entries oldest-first, including\nzero-activity months, so a chart renders at a stable width. `earned` and\n`spent` cover credits and debits only — adjustments and expiries land in\nneither series, because this is the earn-vs-redeem picture rather than a\nnet-change chart. `trend.has_activity` is what the web page gates the\nwhole card on: a flat all-zero chart is worse than no chart.\n\n### Expiry\n\n`balance.expiring_points` is what **newly** expires within\n`balance.expiring_within_days` — the same figure the web banner copy\nclaims and the same one the expiry warning notification sends, so the\nnumber can never disagree with the text beside it. It is **not** the total\ncurrently-expirable pool.\n\nRead it together with `features.points_expiry_enabled`: `0` means\n\"nothing is close\" when expiry is on, and \"this tenant does not expire\npoints\" when it is off. Those want different copy, and the second should\nrender no expiry banner or countdown at all.\n\n### The tax statement\n\n`tax_statement.available` mirrors the web header button: offered only when\nthe viewer actually has taxable redemptions in a year the statement page\nitself offers, because otherwise the link dead-ends on an empty statement.\n`tax_statement.year` is the year that **has** rows, scanned newest-first\nacross the offered range — **not** the current year. In Jan–Apr those are\nusually different, which is exactly when the statement matters most, so a\nclient must link to the year reported rather than to \"this year\".\n\n### Gotchas\n\n* An unrecognised `type` is **ignored** (the response falls back to All)\n  rather than returning nothing — `filters.type` reports what was actually\n  applied, so a client can tell the difference.\n* `per_page` is clamped server-side to 50; `filters.per_page` and\n  `meta.per_page` report the value in force.\n* `source` is a **reference** (`{type, id}`), not a resolved label.\n  Resolving it would mean a polymorphic load per row, and the web rows\n  print the type and nothing more. Fetch the underlying record by\n  reference if a client needs its name.\n* `admin_user` is deliberately absent from adjustment rows: the\n  notification an employee receives says \"An admin added/removed …\" and no\n  web surface names the individual. `notes` **is** present — that is the\n  admin's stated reason, which the same notification already sends.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Narrow to one kind of ledger entry. Blank/absent = All. An unrecognised value is IGNORED (falls back to All) rather than returning an empty list.",
            "schema": {
              "type": "string",
              "enum": [
                "credit",
                "debit",
                "adjustment",
                "expiry"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based page number. Values below 1 are treated as 1.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Rows per page. Clamped to 50; a non-positive or junk value falls back to 20.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's points balance, roll-ups, filter row and transaction page.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "points": {
                      "type": "object",
                      "required": [
                        "viewer",
                        "features",
                        "balance",
                        "activity",
                        "trend",
                        "counts",
                        "type_filters",
                        "filters",
                        "tax_statement",
                        "transactions",
                        "meta"
                      ],
                      "properties": {
                        "viewer": {
                          "$ref": "#/components/schemas/CompanyStorePointsViewer"
                        },
                        "features": {
                          "$ref": "#/components/schemas/CompanyStorePointsFeatures"
                        },
                        "balance": {
                          "$ref": "#/components/schemas/CompanyStorePointsBalance"
                        },
                        "activity": {
                          "$ref": "#/components/schemas/CompanyStorePointsActivity"
                        },
                        "trend": {
                          "$ref": "#/components/schemas/CompanyStorePointsTrend"
                        },
                        "counts": {
                          "$ref": "#/components/schemas/CompanyStorePointsTypeCounts"
                        },
                        "type_filters": {
                          "type": "array",
                          "description": "The pill row, in its display order — All, Credit, Debit, Adjustment, Expiry. The numbers are the same ones `counts` reports.",
                          "items": {
                            "$ref": "#/components/schemas/CompanyStorePointsTypeFilter"
                          }
                        },
                        "filters": {
                          "type": "object",
                          "description": "The filter values the server actually APPLIED (not what was sent).",
                          "properties": {
                            "type": {
                              "type": "string",
                              "nullable": true,
                              "description": "null when no type filter is in force — including when an unknown one was sent and dropped.",
                              "enum": [
                                "credit",
                                "debit",
                                "adjustment",
                                "expiry",
                                null
                              ]
                            },
                            "per_page": {
                              "type": "integer",
                              "description": "The clamped page size in force."
                            }
                          }
                        },
                        "tax_statement": {
                          "$ref": "#/components/schemas/CompanyStorePointsTaxStatement"
                        },
                        "transactions": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/CompanyStorePointsTransaction"
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/CompanyStorePointsPageMeta"
                        }
                      }
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "Native app badge count (the shared api/v1 envelope)."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "One of three refusals, distinguished by `error.code`:\n\n* `insufficient_permissions` — the token lacks `read:company_store`.\n  Checked first, before either gate below.\n* `access_denied` — the Company Store app isn't enabled for this\n  tenant, or this user is outside the app's audience.\n* `store_disabled` — the tenant's admin has paused the store.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "access_denied",
                            "store_disabled"
                          ]
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/company-store/watchlist": {
      "post": {
        "tags": [
          "Company Store"
        ],
        "summary": "Add an item to the watchlist",
        "description": "Saves one store item to the caller's own watchlist (`kind: wishlist`, the\ndefault) or subscribes them to its back-in-stock alert\n(`kind: restock`).\n\n**Idempotent.** Adding an item that is already watched is a `200` with\n`changed: false` and one watch row — never a duplicate, and never a\ntoggle back off. This is the deliberate divergence from the web button;\nsee this file's header.\n\n### What comes back\n\nThe state AFTER the write, read back from the database rather than\nassumed — `wishlisted` and `restock_watch` are resolved together in one\nquery and are named exactly as the catalog reports them, so a client\npatches its cached card field-for-field without a second call.\n\n`wishlist_total` is `Store::DashboardStats#wishlist_total` — the very\nfigure the dashboard's Saved Items header renders and\n`GET /company-store/dashboard` reports as `saved_items.total`. It is the\n**saved-items** scope, not a raw row count: it omits watches whose item an\nadmin has since discontinued, unpublished or restricted to a group this\ncaller isn't in, exactly as the dashboard grid omits them. So saving a\ndiscontinued item is honestly `watching: true` with an unmoved\n`wishlist_total` — the item IS saved, and is genuinely not on the Saved\nItems screen.\n\nNo item card is returned: the caller just tapped the heart on a card it\nalready holds, and this namespace has two card shapes (the shared\ndashboard-grid card and the catalog's extended one). Returning either\nwould hand clients a third shape to reconcile for no new information.\n\n### When `restock` is refused\n\nA back-in-stock alert is accepted only for an item that is currently\nunavailable AND could plausibly come back — the same condition the web\ndetail page renders its button under. `StoreRestockNotifyJob` fires only\non an `out_of_stock → active` flip, so an alert on an in-stock item would\nnever notify anybody, and one on a `discontinued` / `draft` item promises\na restock that is never coming. Both would be confirmed with \"We'll notify\nyou when it's back\", which the store cannot honour, so both are `422\nrestock_not_applicable`.\n\nWishlist saves carry no such restriction — saving an unavailable item is\nthe whole point of a wishlist, and the catalog card's `status` /\n`available` let a client say so.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "item_id"
                ],
                "properties": {
                  "item_id": {
                    "type": "integer",
                    "description": "The StoreItem to watch — the `id` every catalog card reports. Resolved within the caller's own business, so another tenant's id is simply not found.",
                    "example": 412
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "wishlist",
                      "restock"
                    ],
                    "default": "wishlist",
                    "description": "Defaults to `wishlist`, so the common call is just an `item_id`. An unrecognised value is `422 invalid_kind` rather than a silent fallback: a client that sent `restok` meant to write a restock alert, and quietly writing a wishlist row instead is a save it never asked for."
                  },
                  "region_id": {
                    "type": "integer",
                    "description": "Optional, and only relevant while the tenant has active store regions: it selects which region the ADD is judged from, exactly like the web region picker and `GET /company-store/catalog/{id}`. Omit it and the caller's own resolved region applies."
                  }
                }
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "item_id"
                ],
                "properties": {
                  "item_id": {
                    "type": "integer"
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "wishlist",
                      "restock"
                    ],
                    "default": "wishlist"
                  },
                  "region_id": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Watched. `changed` is `true` when this call created the watch, `false` when it was already there.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreWatchlistResponse"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_item_id` — `item_id` was absent, non-numeric, non-positive or sent as an array. Answered separately from the `404` on purpose: \"no such item\" and \"you didn't send an item\" are different client bugs, and an array `item_id` is refused rather than resolved to whichever id sorts first inside it (which would be a write against an item the caller never named).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreWatchlistError"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token"
          },
          "403": {
            "description": "`insufficient_permissions` (the token lacks `write:company_store` — checked first, before the app gate), `access_denied` (the app isn't accessible to this caller), `store_disabled` (an admin paused the store), `region_restricted` (the item belongs to another region) or `region_unavailable` (no store region resolves for this caller).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreWatchlistError"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no such item in this business, **or** an item restricted to an audience group this caller isn't in. Deliberately the same answer with the same copy for both: naming the reason would disclose who is in the group.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreWatchlistError"
                }
              }
            }
          },
          "422": {
            "description": "`invalid_kind` (not one of `wishlist` / `restock`) or `restock_not_applicable` (the item is in stock, or is discontinued / draft, so the alert could never fire). Nothing is written.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreWatchlistError"
                }
              }
            }
          }
        }
      }
    },
    "/company-store/watchlist/{item_id}": {
      "delete": {
        "tags": [
          "Company Store"
        ],
        "summary": "Remove an item from the watchlist",
        "description": "Removes the caller's own watch of one store item — their saved item\n(`kind: wishlist`, the default) or their back-in-stock alert\n(`kind: restock`).\n\n**Idempotent.** Removing a watch that isn't there is a `200` with\n`changed: false`, not a `404`: the caller asked for the item to be off\ntheir watchlist, and it is.\n\nOnly the requested `kind` is removed — an item carrying both a wishlist\nsave and a restock alert keeps the other one, and the response's\n`wishlisted` / `restock_watch` report both.\n\n### No visibility guards here\n\nUnlike the ADD, this operation runs **neither** the region nor the\naudience guard. A watch saved before an admin restricted the item, or\nmoved it to another region, is still the caller's own row — refusing to\nclear it would leave them holding a saved item they can see on no screen\nand cannot delete. The delete is scoped to the caller's own rows in their\nown business, so it can never reach anyone else's watch.\n\nA `404` here therefore means only one thing: no such item exists in this\nbusiness (in which case no watch of it can exist either, since watches are\ndeleted with their item).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "item_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The StoreItem id every catalog card reports. Digit-constrained by the route, so a non-numeric value does not match this operation at all."
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "wishlist",
                "restock"
              ],
              "default": "wishlist"
            },
            "description": "A query parameter rather than a body field, because a DELETE body is not reliably forwarded by every client stack. An unrecognised value is `422 invalid_kind` and nothing is deleted."
          }
        ],
        "responses": {
          "200": {
            "description": "Not watched. `changed` is `true` when this call removed a watch, `false` when there was nothing to remove.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreWatchlistResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token"
          },
          "403": {
            "description": "`insufficient_permissions`, `access_denied` or `store_disabled` — see the ADD operation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreWatchlistError"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no such item in this business.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreWatchlistError"
                }
              }
            }
          },
          "422": {
            "description": "`invalid_kind` — not one of `wishlist` / `restock`. Nothing is deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreWatchlistError"
                }
              }
            }
          }
        }
      }
    },
    "/recognitions/config": {
      "get": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Recognitions configuration",
        "description": "**The first call a client makes.** One request returns everything needed\nto render the role-adaptive Recognitions shell and both composers, so a\nclient never has to hardcode the points economy, the message limits, or\nwhich controls a given viewer may use.\n\nEvery value here is read from the definition the **write path already\nenforces** — the give guard, the reviewer rule, the moderation rule, the\nprogram eligibility predicate, the model validations, the allowance\nmodel. So a control you render from this payload is a control whose POST\nthe server will accept. Re-deriving any of it client-side is the drift\nthis endpoint exists to remove.\n\n**Persona-aware, not persona-branched.** Every caller receives the SAME\nkeys — nothing is omitted by role. Read `permissions` and `features`\nrather than probing for a missing key. What varies is the VALUE of those\nbooleans and of the per-viewer economy figures.\n\n### The five answers\n\n* **`module_enabled` / `module_label`** — is Recognize on here, and what\n  is it called. Terminology varies per org (\"Recognize\", \"Kudos\"), and\n  the label comes from the app record, so a console rename reaches the\n  app with no client release. `module_enabled` is always `true` in a 200\n  (the endpoint 403s otherwise) and is reported so one client model\n  covers both answers.\n* **`viewer_role` + `permissions`** — `employee` / `manager` / `admin`,\n  and the seven affordance booleans behind it. `manager` means *has\n  direct reports*; `admin` means a business admin/owner or a Recognitions\n  app-admin, and outranks `manager`.\n* **`features`** — the TENANT switches that decide which surfaces exist\n  at all. Kept separate from `permissions` on purpose: hide a tab on\n  `features`, disable a button on `permissions`. Collapsing them means an\n  employee can't tell \"this org doesn't do nominations\" from \"no program\n  accepts me\".\n* **`economy` / `limits` / `visibility_options`** — the real, admin\n  configurable numbers and the values the server will actually accept.\n* **`values` / `tags` / `cards` / `programs`** — the give composer's and\n  the nominate picker's option catalogs, shared verbatim with the web and\n  mobile give forms.\n\n### Composing a give from this payload\n\n`values`, `tags` and `cards` are exactly what the web give form offers.\nA card is submitted as its `award_template_id`, which is an **integer**\nfor a tenant-authored design and the **string** `\"central:<slug>\"` for a\ncentral-gallery one (the server materialises the gallery art into a\ntenant asset on submit) — send the field verbatim rather than\nreconstructing it. `cards.gallery_truncated` is true when the gallery\npage came back full, meaning the catalog holds designs this payload did\nnot list; say so in a picker's \"no match\" state.\n\n`visibility_options` lists the composer's visibility **and** anonymity\ntoggles together, because they are one row of controls on the screen —\neach row names the `param` a client submits it under (`visibility` or\n`is_anonymous`). `department` is the prototype's \"my_department\"; the\nwire value is `department` because that is what the server stores.\n`private` is offered only when it is the tenant's own default, and\n`anonymous` only while the tenant allows anonymous recognition. The\nlegacy `team` synonym is never offered.\n\n### Composing a nomination\n\n`programs.items` is the nominate picker's vocabulary — trimmed to what a\npicker needs, with `can_nominate` resolved per viewer by the same\npredicate the submit path enforces and `nomination_block_reason`\nexplaining a `false` in the viewer's own words (**render it** — a\ndisabled row with no reason is a dead end). Automatic milestone programs\nare never listed: nobody nominates in them. The list is bounded by\n`programs_limit`; `total_count` and `truncated` report the rest, and the\nbrowsable, paginated surface is `GET /recognitions/programs`. When\naward requests (\"Model A\") are off, the list is empty and\n`permissions.can_nominate` is `false`.\n\n### Cost\n\nFlat. Nothing in the payload costs a query per program, value or card,\nso this is safe to call on every launch. The central card gallery is\ncached and failure-tolerant — an unreachable gallery yields an empty\n`cards.gallery` rather than an error.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "programs_limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 12
            },
            "description": "How many nominatable programs the picker vocabulary carries, clamped to 50. `programs.total_count` and `programs.truncated` report the rest. This is a composer vocabulary, not a browsable page — use `GET /recognitions/programs` to page through them."
          }
        ],
        "responses": {
          "200": {
            "description": "Configuration retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "config"
                  ],
                  "properties": {
                    "config": {
                      "$ref": "#/components/schemas/RecognitionConfig"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "The caller's unread notification count, for the app badge.",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Recognitions app is not enabled for the tenant, or this user is outside the app's audience (error code `access_denied`)."
          }
        }
      }
    },
    "/recognitions/dashboard": {
      "get": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Recognitions dashboard",
        "description": "The native-client mirror of the web Recognition dashboard. Every number\nand list is produced by the same query object that backs the web page, so\nthe two surfaces cannot drift.\n\n**The response is persona- and setting-aware.** Keys that do not apply\nare **absent** (not null), exactly as the web page renders no card — read\n`viewer` and `features` to know which shape you received:\n\n* **Every viewer** gets `viewer`, `features`, the `recognition_received`\n  and `recognition_given` stats, `recognition`, `my_awards`,\n  `trending_recognition` and `top_recipients_this_month`.\n* **Givers** — anyone the tenant lets give peer recognition — additionally\n  get `people_to_recognize`, the ranked nudge strip (up to 8 chips) the\n  web feed shows above the stream. Absent for a viewer whose give would\n  be refused.\n* **Reviewers** — a recognition admin, or anyone with direct reports —\n  additionally get `pending_approvals`, the queue the web page banners\n  above everything else. Recognition stays hidden from the recipient\n  until the reviewer decides, so surfacing this promptly matters.\n* **Award requests on** (Model A — the default) adds\n  `stats.nominations`, `my_nominations` and `active_programs`.\n* **Award cycles on** (Model B — off by default) adds `running_program`:\n  the time-boxed cycle currently accepting nominations, or `null` when\n  none is open.\n\nVisibility is enforced per row: `recognition` merges the tenant's public\nawards with only the posts this viewer may see (public, own,\nsame-department, same-team), and a group give collapses to ONE row naming\nevery recipient. Anonymous recognition never names the giver, and\nautomated lifecycle awards (anniversaries, birthdays) present as\n`System (Automated)` rather than the system principal.\n\nList sizes mirror the web widgets: 5 rows each, except\n`trending_recognition` (3), `active_programs` (3) and\n`people_to_recognize` (8).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Dashboard retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "dashboard": {
                      "type": "object",
                      "required": [
                        "viewer",
                        "features",
                        "stats",
                        "recognition",
                        "my_awards",
                        "trending_recognition",
                        "top_recipients_this_month"
                      ],
                      "properties": {
                        "viewer": {
                          "type": "object",
                          "description": "Who the caller is, for the affordances the client shows.",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 412
                            },
                            "name": {
                              "type": "string",
                              "example": "Anthony Rivera"
                            },
                            "image": {
                              "type": "string",
                              "nullable": true,
                              "description": "Absolute avatar URL, or null when it can't be resolved.",
                              "example": "https://acme.workforce.mangoapps.com/rails/active_storage/blobs/redirect/ey.../a.jpg"
                            },
                            "can_give_recognition": {
                              "type": "boolean",
                              "description": "Whether this viewer may give peer recognition at all — the SAME guard the submit path enforces. Hide the Give affordance when false; the POST would be rejected.",
                              "example": true
                            },
                            "is_reviewer": {
                              "type": "boolean",
                              "description": "True for a recognition admin OR anyone with direct reports. Determines whether `pending_approvals` is present.",
                              "example": false
                            },
                            "is_recognition_admin": {
                              "type": "boolean",
                              "description": "Business admin (or above), or a per-app admin for Recognitions. Widens the approval queue from \"assigned to me\" to the whole tenant.",
                              "example": false
                            }
                          }
                        },
                        "features": {
                          "type": "object",
                          "description": "The tenant's nomination surfaces. Tells the client which optional sections to expect, so a disabled surface never has to be inferred from a missing key.",
                          "properties": {
                            "award_requests_enabled": {
                              "type": "boolean",
                              "description": "Model A — ad-hoc nominate → approve → award. Default true.",
                              "example": true
                            },
                            "award_cycles_enabled": {
                              "type": "boolean",
                              "description": "Model B — time-boxed award cycles. Default false.",
                              "example": false
                            }
                          }
                        },
                        "stats": {
                          "type": "object",
                          "description": "The three stat tiles. Received and Given each roll an award count and a shout-out count into ONE total — render `total` as the figure and the split as its sub-line, which is how the web tile reads.",
                          "properties": {
                            "recognition_received": {
                              "type": "object",
                              "description": "Recognition the caller received (awards + shout-outs).",
                              "properties": {
                                "total": {
                                  "type": "integer",
                                  "example": 21
                                },
                                "awards": {
                                  "type": "integer",
                                  "example": 12
                                },
                                "shout_outs": {
                                  "type": "integer",
                                  "example": 9
                                }
                              }
                            },
                            "recognition_given": {
                              "type": "object",
                              "description": "Recognition the caller gave (awards + shout-outs).",
                              "properties": {
                                "total": {
                                  "type": "integer",
                                  "example": 62
                                },
                                "awards": {
                                  "type": "integer",
                                  "example": 28
                                },
                                "shout_outs": {
                                  "type": "integer",
                                  "example": 34
                                }
                              }
                            },
                            "nominations": {
                              "type": "object",
                              "description": "Present only while award requests are enabled. `awaiting_approval` counts how many of the caller's OWN submitted nominations are still waiting on a decision (pending or under review) — it is a subset of `submitted`.",
                              "properties": {
                                "submitted": {
                                  "type": "integer",
                                  "example": 5
                                },
                                "awaiting_approval": {
                                  "type": "integer",
                                  "example": 2
                                }
                              }
                            }
                          }
                        },
                        "people_to_recognize": {
                          "type": "array",
                          "description": "**Givers only** — present when `viewer.can_give_recognition` is true, absent otherwise (the web strip is gated on the same rule, and a chip whose give would be refused is worse than no chip).\nThe \"People to recognize\" nudge strip: up to 8 colleagues this caller is most likely to want to recognize next, from the same query object as the web strip (`Recognition::SuggestedRecipientsQuery`).\n**The order IS the signal — render it as given, never re-sort.** Ranking: people the caller recently shared SHIFTS with (most shifts first — a collaboration signal a standalone recognition tool can't see), then their direct reports, then same-department colleagues to fill the strip out. Direct reports are deliberately eligible for the shift tier too; recognizing your own report is a primary use case.\nTapping a chip should open the give composer with that person prefilled. An empty array means \"nobody to suggest\" (a caller with no shared shifts, no reports and no department) — render no strip, not an empty one.\nThe strip's trailing \"Someone else\" affordance (open the composer with no recipient) is client chrome and carries no row here — do not expect one.",
                          "items": {
                            "allOf": [
                              {
                                "$ref": "#/components/schemas/RecognitionPerson"
                              },
                              {
                                "type": "object",
                                "properties": {
                                  "first_name": {
                                    "type": "string",
                                    "description": "The compact chip label — the strip shows a face and a first name, not a full name. Honors a preferred name, and falls back to the full display name rather than ever being blank.",
                                    "example": "Maya"
                                  },
                                  "department": {
                                    "type": "string",
                                    "nullable": true,
                                    "description": "The chip's secondary line; null when the person has no department.",
                                    "example": "Store Operations"
                                  },
                                  "reason": {
                                    "type": "string",
                                    "nullable": true,
                                    "description": "Why this person is suggested, as the strip's subtext. Only the shift-coworker tier carries one today; null for a direct-report or department fill suggestion.",
                                    "example": "Worked 3 shifts together"
                                  },
                                  "work_anniversary": {
                                    "type": "boolean",
                                    "description": "TODAY is this person's work anniversary — the highlight state (the web strip tints the chip and appends \"anniversary\"). Recognize-them-now, not a date to print.",
                                    "example": false
                                  }
                                }
                              }
                            ]
                          }
                        },
                        "pending_approvals": {
                          "type": "object",
                          "description": "Reviewers only. Recognition waiting on THIS viewer's decision. The same reviewer rule and scopes as the pending-approvals page, so this count always equals the queue it links to.",
                          "properties": {
                            "total": {
                              "type": "integer",
                              "example": 3
                            },
                            "posts": {
                              "type": "integer",
                              "example": 2
                            },
                            "nominations": {
                              "type": "integer",
                              "example": 1
                            },
                            "oldest_pending_at": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true,
                              "description": "When the longest-waiting item arrived; null when the queue is empty."
                            }
                          }
                        },
                        "recognition": {
                          "type": "array",
                          "description": "The merged activity stream — public awards plus the posts this viewer may see, newest first (max 5).",
                          "items": {
                            "type": "object",
                            "properties": {
                              "type": {
                                "type": "string",
                                "enum": [
                                  "award",
                                  "recognition_post"
                                ],
                                "example": "award"
                              },
                              "id": {
                                "type": "integer",
                                "example": 9012
                              },
                              "title": {
                                "type": "string",
                                "nullable": true,
                                "description": "The award's title; null for a peer shout-out.",
                                "example": "Above and Beyond"
                              },
                              "message": {
                                "type": "string",
                                "nullable": true,
                                "example": "Stayed late three nights to get the store reset done."
                              },
                              "points": {
                                "type": "integer",
                                "description": "Value in reward points. Awards store dollars and are converted here; peer posts carry points directly.",
                                "example": 250
                              },
                              "recipient": {
                                "$ref": "#/components/schemas/RecognitionPerson"
                              },
                              "group_recipients": {
                                "type": "array",
                                "nullable": true,
                                "description": "Present ONLY on a collapsed group give — every recipient of the one recognition, so the client can render \"X and N others\". Null otherwise.",
                                "items": {
                                  "$ref": "#/components/schemas/RecognitionPerson"
                                }
                              },
                              "giver": {
                                "allOf": [
                                  {
                                    "$ref": "#/components/schemas/RecognitionPerson"
                                  }
                                ],
                                "nullable": true,
                                "description": "Null when the recognition is anonymous. An automated lifecycle award reports `System (Automated)` with a null id."
                              },
                              "anonymous": {
                                "type": "boolean",
                                "example": false
                              },
                              "program": {
                                "type": "string",
                                "nullable": true,
                                "example": "Spot Awards"
                              },
                              "category": {
                                "type": "string",
                                "nullable": true,
                                "example": "Customer Focus"
                              },
                              "company_value": {
                                "type": "string",
                                "nullable": true,
                                "example": "Teamwork"
                              },
                              "occurred_at": {
                                "type": "string",
                                "format": "date-time"
                              }
                            }
                          }
                        },
                        "my_awards": {
                          "type": "array",
                          "description": "Awards the caller received, newest first (max 5). Finalized only — revoked and expired awards are excluded, so this list always equals `stats.recognition_received.awards` (up to the cap).\n**Page the rest with `GET /recognitions/my_recognition?tab=awards`, not `?tab=received`.** This card is an AWARDS-only preview; `received` merges peer shout-outs in, so sending its \"View all\" there shows rows the card never held. It also means the figure to render beside this card is `stats.recognition_received.awards` — NOT `stats.recognition_received.total`, which is awards + shout-outs. A recipient with shout-outs and no awards legitimately has an empty `my_awards`, a non-zero `total`, and nothing to \"view all\" of.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 771
                              },
                              "title": {
                                "type": "string",
                                "example": "Customer Hero"
                              },
                              "message": {
                                "type": "string",
                                "nullable": true,
                                "example": "Turned a churn risk into a multi-year renewal."
                              },
                              "points": {
                                "type": "integer",
                                "example": 250
                              },
                              "giver": {
                                "allOf": [
                                  {
                                    "$ref": "#/components/schemas/RecognitionPerson"
                                  }
                                ],
                                "nullable": true,
                                "description": "Null when the award was given anonymously."
                              },
                              "program": {
                                "type": "string",
                                "nullable": true,
                                "example": "Excellence Awards"
                              },
                              "category": {
                                "type": "string",
                                "nullable": true,
                                "example": "Leadership"
                              },
                              "awarded_at": {
                                "type": "string",
                                "format": "date-time"
                              }
                            }
                          }
                        },
                        "my_nominations": {
                          "type": "array",
                          "description": "Present only while award requests are enabled. Nominations the caller submitted, newest first (max 5).",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 335
                              },
                              "title": {
                                "type": "string",
                                "example": "Renewal save of the quarter"
                              },
                              "points": {
                                "type": "integer",
                                "description": "What the nomination is worth right now, in reward POINTS — the approved figure once a reviewer has set one, the requested figure until then. 0 when the nomination was filed without a value.",
                                "example": 250
                              },
                              "requested_points": {
                                "type": "integer",
                                "nullable": true,
                                "description": "The value the nominator asked for, in reward points. Null (not 0) when none was specified.",
                                "example": 250
                              },
                              "approved_points": {
                                "type": "integer",
                                "nullable": true,
                                "description": "The value a reviewer granted, in reward points. Null until the nomination is approved.",
                                "example": 250
                              },
                              "nominee": {
                                "$ref": "#/components/schemas/RecognitionPerson"
                              },
                              "program": {
                                "type": "string",
                                "nullable": true,
                                "example": "Excellence Awards"
                              },
                              "category": {
                                "type": "string",
                                "nullable": true,
                                "example": "Outstanding Performance"
                              },
                              "status": {
                                "type": "string",
                                "enum": [
                                  "pending",
                                  "under_review",
                                  "approved",
                                  "rejected",
                                  "cancelled"
                                ],
                                "example": "pending"
                              },
                              "status_label": {
                                "type": "string",
                                "description": "The humanized status the web row prints.",
                                "example": "Pending"
                              },
                              "awaiting_approval": {
                                "type": "boolean",
                                "description": "True while the status is pending or under review.",
                                "example": true
                              },
                              "submitted_at": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true
                              }
                            }
                          }
                        },
                        "trending_recognition": {
                          "type": "array",
                          "description": "The most-engaged PUBLIC recognition of the last 24 hours (max 3). Same row shape as `recognition`, plus the engagement counts the web card prints. Deliberately public-only: this is org-wide social proof, so it must not surface a department/team/private recognition.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "type": {
                                "type": "string",
                                "example": "recognition_post"
                              },
                              "id": {
                                "type": "integer",
                                "example": 4471
                              },
                              "title": {
                                "type": "string",
                                "nullable": true,
                                "description": "The award's title; null for a peer shout-out.",
                                "example": "Above and Beyond"
                              },
                              "message": {
                                "type": "string",
                                "nullable": true
                              },
                              "points": {
                                "type": "integer",
                                "example": 25
                              },
                              "recipient": {
                                "$ref": "#/components/schemas/RecognitionPerson"
                              },
                              "group_recipients": {
                                "type": "array",
                                "nullable": true,
                                "description": "Present ONLY on a collapsed group give — every recipient of the one recognition. Null otherwise.",
                                "items": {
                                  "$ref": "#/components/schemas/RecognitionPerson"
                                }
                              },
                              "giver": {
                                "allOf": [
                                  {
                                    "$ref": "#/components/schemas/RecognitionPerson"
                                  }
                                ],
                                "nullable": true
                              },
                              "anonymous": {
                                "type": "boolean",
                                "example": false
                              },
                              "program": {
                                "type": "string",
                                "nullable": true,
                                "example": "Spot Awards"
                              },
                              "category": {
                                "type": "string",
                                "nullable": true,
                                "example": "Customer Focus"
                              },
                              "company_value": {
                                "type": "string",
                                "nullable": true,
                                "example": "Teamwork"
                              },
                              "reactions_count": {
                                "type": "integer",
                                "example": 14
                              },
                              "comments_count": {
                                "type": "integer",
                                "example": 3
                              },
                              "occurred_at": {
                                "type": "string",
                                "format": "date-time"
                              }
                            }
                          }
                        },
                        "active_programs": {
                          "type": "array",
                          "description": "Present only while award requests are enabled. The programs a nomination can actually be filed under (max 3). Milestone programs are excluded — anniversaries and birthdays are granted automatically and are never nominated.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 12
                              },
                              "name": {
                                "type": "string",
                                "example": "Spot Awards"
                              },
                              "description": {
                                "type": "string",
                                "nullable": true,
                                "example": "Peer-to-peer recognition for great work, any time."
                              },
                              "program_type": {
                                "type": "string",
                                "enum": [
                                  "peer_to_peer",
                                  "manager_to_employee",
                                  "achievement_based"
                                ],
                                "example": "peer_to_peer"
                              },
                              "icon": {
                                "type": "string",
                                "description": "Font Awesome class derived from the program type — the same glyph the web card shows. Programs carry no icon of their own.",
                                "example": "fas fa-users"
                              },
                              "can_nominate": {
                                "type": "boolean",
                                "description": "Server-side eligibility for THIS caller. False for a manager-only program a non-manager is viewing, or one whose criteria they don't meet — don't open the nominate form; the POST would be rejected.",
                                "example": true
                              }
                            }
                          }
                        },
                        "top_recipients_this_month": {
                          "type": "array",
                          "description": "The month's most-recognized people (max 5), counting BOTH awards and peer shout-outs — recognition is recorded on two tables and a single-table leaderboard undercounts it.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "rank": {
                                "type": "integer",
                                "description": "1-based position in this list.",
                                "example": 1
                              },
                              "user": {
                                "allOf": [
                                  {
                                    "$ref": "#/components/schemas/RecognitionPerson"
                                  }
                                ],
                                "nullable": true,
                                "description": "Null when the recipient has since left the business. Their count still stands, so `name` is always sent."
                              },
                              "name": {
                                "type": "string",
                                "example": "Patrick Smith"
                              },
                              "count": {
                                "type": "integer",
                                "example": 4
                              }
                            }
                          }
                        },
                        "running_program": {
                          "type": "object",
                          "nullable": true,
                          "description": "Present only while award cycles are enabled. The time-boxed cycle currently accepting nominations — null when none is open, in which case render nothing rather than an empty card. When several are open, the one closing soonest wins.",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 7
                            },
                            "title": {
                              "type": "string",
                              "example": "Employee of the Quarter — Q3"
                            },
                            "description": {
                              "type": "string",
                              "nullable": true,
                              "example": "Recognize a standout performer for the quarter."
                            },
                            "icon": {
                              "type": "string",
                              "description": "Cycles carry no icon; the web banner uses the trophy.",
                              "example": "fas fa-trophy"
                            },
                            "days_left": {
                              "type": "integer",
                              "description": "Whole days until the window closes, floored at 0 (0 = closes today).",
                              "example": 5
                            },
                            "closes_at": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "total_nominations": {
                              "type": "integer",
                              "description": "Nominations pooled in this cycle so far, from everyone.",
                              "example": 14
                            },
                            "can_nominate": {
                              "type": "boolean",
                              "description": "Both halves of the server's own gate: the cycle is still accepting AND this viewer may give recognition.",
                              "example": true
                            },
                            "program_id": {
                              "type": "integer",
                              "example": 12
                            },
                            "program_name": {
                              "type": "string",
                              "example": "Mango Champions"
                            }
                          }
                        }
                      }
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "The caller's unread notification count, for the app badge.",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Recognitions app is not enabled for the tenant, or this user is outside the app's audience (error code `access_denied`)."
          }
        }
      }
    },
    "/recognitions/employee_suggestions": {
      "get": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Employee suggestions for giving recognition",
        "description": "The colleagues this caller may pick as a recognition **recipient** — the\nnative-client mirror of the web recipient typeahead\n(`GET /recognition/employee_suggestions`), which backs the give composer,\nboth nominate forms and the manager Quick Award.\n\nBoth surfaces read ONE query object\n(`Recognition::EmployeeSuggestionsQuery`), so who is suggestible, what a\nsearch matches and the order rows come back in are the same code. A\ncolleague findable in the app is findable here, and vice versa.\n\n**Givers only.** This is the one endpoint in the Recognitions API gated on\ngiving access, because a recipient roster is only ever read in order to\ngive. A tenant that switches peer recognition off leaves giving to admins\nand managers, and the submit path enforces exactly the same rule — so a\ncaller is never handed a roster whose give would be refused. Everyone else\ngets 403 `giving_not_allowed` rather than a browsable directory of their\ncolleagues.\n\n**Who is listed:** active members of *this* business only. Never the\ncaller (the submit path strips them out of the recipient set anyway),\nnever a member the admin deactivated, never a service / AI-agent\nprincipal, never another tenant's user.\n\n**Search (`q`)** matches, case-insensitively and on any substring, against\nthe `name` column, `first_name`, `last_name`, `preferred_name`, `email`,\nand the `\"<first> <last>\"` / `\"<preferred> <last>\"` forms. LIKE wildcards\n(`%`, `_`) are treated as literal characters. A blank `q` returns the\nwhole roster alphabetically, which is what a picker opens with.\n\n**Suggestions, not a ranked feed.** The work-context nudge (\"People to\nrecognize\" — recent shift coworkers, then direct reports, then department)\nis a different, un-paginated list and ships on\n`GET /recognitions/dashboard`; it is not duplicated here.\n\n`scope=direct_reports` applies the Quick Award narrowing: for a\n**non-admin** it restricts the roster to their own direct reports, which is\nthe recipient rule the Quick Award submit path enforces. An admin may award\nanyone, so nothing is narrowed for them — read `meta.direct_reports_only`\n(not the `scope` you sent) to decide \"your team only\" copy.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Search term. Matched against name, first/last/preferred name, email and the two \"<first> <last>\" / \"<preferred> <last>\" forms. Blank means the whole roster.",
            "example": "maya"
          },
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "direct_reports"
              ],
              "default": "all"
            },
            "description": "`direct_reports` narrows a non-admin caller to their own reports (the Quick Award recipient rule). An unrecognized value falls back to `all` rather than erroring."
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            },
            "description": "1-based page number."
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            },
            "description": "Rows per page, clamped to 100. Defaults to 25, matching the web picker, so the two surfaces page identically."
          }
        ],
        "responses": {
          "200": {
            "description": "Suggestions retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "employees",
                    "meta"
                  ],
                  "properties": {
                    "employees": {
                      "type": "array",
                      "description": "Suggestible colleagues for this page, ordered by name (with id as a stable tiebreak, so no one appears on two pages).",
                      "items": {
                        "$ref": "#/components/schemas/RecognitionEmployeeSuggestion"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/RecognitionEmployeeSuggestionsMeta"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "Piggybacked notification badge count."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token"
          },
          "403": {
            "description": "Either the Recognitions app isn't accessible to this caller\n(`access_denied`), or the caller may not give recognition\n(`giving_not_allowed`) — the same rule the submit path enforces. No\nroster is included in a refusal.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "access_denied",
                            "giving_not_allowed"
                          ]
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/recognitions/quick_award": {
      "get": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Quick Award form options",
        "description": "**Everything the Quick Award form renders from**, in one call — the native\nmirror of the web \"Give an Instant Award\" screen (Team ▸ Quick Award) and\nof the native design's **Team ▸ + ▸ Quick Award** sheet.\n\nA Quick Award is a manager awarding points to a team member **instantly,\nwith no approval step** — the counterpart to a nomination, which goes\nthrough review. It draws against the manager's giving budget when the\ntenant configures one.\n\nBoth this endpoint and the web page read one shared object\n(`Recognition::QuickAwardOptions`), and the POST below resolves its\ndefault program through the same object — so what is **offered** here is\nwhat that screen offers, and what the server will **accept**.\n\n### Who may call it\n\n**Reviewers only, in a tenant that has Quick Awards on** — a Recognitions\nadmin, or anyone with direct reports. That is exactly the pair\n`GET /recognitions/config` reports as `permissions.can_quick_award`, so\nread that flag to decide whether to show the affordance at all rather than\nprobing this endpoint. The two refusals are separate codes on purpose:\n`feature_disabled` means the org doesn't do Quick Awards (hide it),\n`forbidden` means this person isn't a reviewer (it isn't theirs).\n\nPer-**recipient** authority is a different question, answered by\n`recipient_scope`: an admin may award anyone, a manager only their own\ndirect reports — which is the rule the POST enforces on the row. Call\n`GET /recognitions/employee_suggestions?scope={recipient_scope}` to fill\nthe picker and it can never offer somebody the POST would reject.\n\n### Every figure is in POINTS\n\nReward points are the program-wide unit, shared with peer gives and the\nCompany Store. The columns store dollars (1 pt = 1¢) and the server\nconverts on the way in and out, so a client never handles dollars —\n`amount.points_per_dollar` is there for the rare screen that shows a\ncurrency figure.\n\n### Composing the form from this payload\n\n* **`amount`** — the input's floor, ceiling and pre-filled value, plus\n  `presets`, the web's own one-tap ladder already filtered to the tenant's\n  cap. Never widen the input past `max_points`; the POST refuses it.\n* **`limits`** — what the two text fields will actually accept. Both are\n  optional (the server substitutes a default when either is blank), but\n  `message_min` is the one that surprises callers: a note that IS sent has\n  to clear 10 characters, or the POST answers 422 `invalid`.\n* **`programs`** — active, in-window programs by name, each with only its\n  **active** categories. A category carries its own point range\n  (`min_points` / `max_points` / `default_points`, `null` meaning\n  unbounded on that side) and a program may carry\n  `per_award_limit_points`, its own ceiling on a single award. Bound the\n  amount input by the tightest of the three.\n* **`require_category`** — whether the category field is mandatory. Render\n  the asterisk from this; the server enforces it.\n* **`default_program_id`** — what \"Default Program\" resolves to, and the\n  program the POST funds the award from when `program_id` is omitted. The\n  `budget` below is that program's budget, so the two agree.\n* **`budget`** — the manager's remaining giving budget, every figure in\n  points. `{ \"enabled\": false }` for most tenants: businesses without\n  group budgets are unaffected. `blocked` is the web submit button's own\n  disabled condition — disable Give on it rather than re-deriving\n  \"exhausted or nothing left\".\n* **`recent_awards`** — this manager's last few awards in this tenant, the\n  web sidebar's confirmation list. Deliberately smaller than a feed card;\n  the full card is one request away at `GET /recognitions/awards/{id}`.\n\n### Cost\n\nFlat. Nothing costs a query per program or per category, so this is safe\nto call every time the sheet opens.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The form's options",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "quick_award"
                  ],
                  "properties": {
                    "quick_award": {
                      "$ref": "#/components/schemas/RecognitionQuickAwardForm"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "The caller's unread notification count, for the app badge.",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Recognitions app isn't accessible to this caller (`access_denied`), the tenant has Quick Awards switched off (`feature_disabled`), or the caller is not a reviewer (`forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionQuickAwardRefusal"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Give a Quick Award",
        "description": "**Award points to a team member instantly, with no approval step.** The\nnative mirror of the web \"Give an Instant Award\" submit\n(`POST /recognition/quick_award`) and of the mobile Quick Award form.\n\nRuns `Recognition::QuickAwardService`, the canonical creator every non-web\nquick-award surface runs, so the direct-report recipient rule, the amount\nbounds, the anti-gaming guard, the program per-award cap, the category\nrequirement and the per-manager group budget are the same code everywhere.\nThe award is created **active** — it is live the moment this returns 201,\nthe points are credited to the recipient's Company Store balance, and the\nrecipient is notified.\n\nGated identically to the GET above. Fill every picker from it.\n\n### The fields\n\n`recipient_id` and `amount` are the only required ones — that is the\nminimum the native sheet collects. Everything else has a documented\nserver-side default:\n\n* **`amount`** is in **POINTS**, within `amount.min_points` ..\n  `amount.max_points`, and also within the chosen program's\n  `per_award_limit_points` and the chosen category's range when either is\n  set.\n* **`program_id`** omitted funds the award from `default_program_id`. A\n  `program_id` that is not an active program in this tenant is refused\n  with `invalid_program` — it is never silently swapped for the default,\n  which would charge a program the caller did not choose.\n* **`category_id`** must belong to the chosen program — a category id from\n  a *different* program is silently ignored rather than refused, matching\n  the web form, where a category can only be picked after its program.\n  Required when `require_category` is true.\n* **`title`** blank becomes `\"Quick Award from {giver name}\"`.\n* **`message`** blank becomes `\"Great work! Keep it up.\"`. A value that\n  IS sent must clear `limits.message_min` (10 characters).\n* **`is_public`** **defaults to `true`**, because the web checkbox is\n  pre-checked — send `false` explicitly to keep the award off the\n  recognition feed and out of any connected Slack/Teams channel.\n* **`anniversary_years`** is what the anniversary roster's **Recognize**\n  action adds, and the ONLY thing that makes this award count as that\n  person's work-anniversary recognition — send the row's own `years`.\n  Omit it for an ordinary spot award. See the field description for what\n  it records.\n\n### The response\n\n`award` is the same **feed card** `GET /recognitions/feed` and\n`GET /recognitions/awards/{id}` return, so the card a client inserts\noptimistically is the card it gets back on the next refresh. `message` is\nthe web flash word for word — show it as-is. `budget` is the state\n**after** the spend, so a budget header refreshes without a second call.\n\nUnlike a peer give, a Quick Award never lands in a pending state: there is\nno moderation hold and no approval routing, so a 201 always means the\naward is live.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecognitionQuickAwardRequest"
              },
              "examples": {
                "minimal": {
                  "summary": "The native sheet's minimum — a person and an amount",
                  "value": {
                    "recipient_id": 412,
                    "amount": 500
                  }
                },
                "full": {
                  "summary": "The web form, every field",
                  "value": {
                    "recipient_id": 412,
                    "program_id": 7,
                    "category_id": 31,
                    "amount": 2500,
                    "title": "Shipped the migration",
                    "message": "You carried the migration all weekend — thank you.",
                    "is_public": true
                  }
                },
                "private": {
                  "summary": "Keep it off the feed",
                  "value": {
                    "recipient_id": 412,
                    "amount": 500,
                    "is_public": false
                  }
                },
                "anniversary": {
                  "summary": "The anniversary roster's Recognize action — records the recognition",
                  "value": {
                    "recipient_id": 412,
                    "amount": 500,
                    "title": "10 Year Work Anniversary",
                    "anniversary_years": 10
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The award is live, the points are credited",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "award",
                    "message"
                  ],
                  "properties": {
                    "award": {
                      "$ref": "#/components/schemas/RecognitionQuickAwardCard"
                    },
                    "message": {
                      "type": "string",
                      "description": "The web flash, word for word. Safe to show as-is; a 201 always means the award is live.",
                      "example": "Successfully awarded Priya Nair with 500 points! They've been credited to their account."
                    },
                    "budget": {
                      "$ref": "#/components/schemas/RecognitionQuickAwardBudget"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "The caller's unread notification count, for the app badge.",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`access_denied`, `feature_disabled` or `forbidden` — the same three refusals, with the same wording, as the GET above. A rule that guarded only the read would be a rule a direct POST walked past.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionQuickAwardRefusal"
                }
              }
            }
          },
          "422": {
            "description": "The award was refused and **nothing was written**. `error.code` says\nwhich rule, so a client can highlight the offending field instead of\nshowing a generic banner:\n\n| code | the field to fix |\n|---|---|\n| `invalid_recipient` | `recipient_id` — no such person in this tenant |\n| `recipient_not_permitted` | `recipient_id` — not one of your direct reports (an admin may award anyone) |\n| `invalid_amount` | `amount` — zero or negative |\n| `amount_over_limit` | `amount` — above the tenant cap, the program's `per_award_limit_points`, or the category's range. `details` carries the bounds. |\n| `category_required` | `category_id` — this tenant requires one |\n| `governance_blocked` | none — the tenant's monthly give cap or duplicate-recipient cooldown. `message` names the limit and when it lifts; show it verbatim. |\n| `budget_exceeded` | `amount` — the manager's giving budget can't cover it. `budget` reports what's left. |\n| `invalid_program` | `program_id` — you named a program that isn't an active one in this tenant. Re-fetch the picker from `GET /recognitions/quick_award`. Omit `program_id` to award from the default program instead. |\n| `no_program` | none — the tenant has no active recognition program. An admin has to create one. |\n| `invalid` | a model validation, most often `message` below `limits.message_min`. `message` carries the validation's own wording. |\n\nNote `recipient_not_permitted` is **422, not 403**: the endpoint *is*\nfor this caller, it is the recipient field that is wrong — highlight\nthe picker rather than hiding the screen.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionQuickAwardError"
                }
              }
            }
          },
          "500": {
            "description": "`internal_error` — an unexpected failure while creating the award. Distinguished from the 422s so a client retries rather than telling the manager their input was wrong.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionQuickAwardError"
                }
              }
            }
          }
        }
      }
    },
    "/recognitions/my_recognition": {
      "get": {
        "tags": [
          "Recognitions"
        ],
        "summary": "My Recognition — the caller's own all-time record",
        "description": "The native-client mirror of the web **My Recognition** page\n(`/recognition/my_recognition`). Both surfaces read the same query object\n(`Recognition::MyRecognitionQuery`), so the rows, the totals and the\nreward-points wallet cannot drift between them.\n\n**Always self-scoped.** There is no `user_id` parameter and one sent\nanyway is ignored — this endpoint can only ever return the token holder's\nown recognition. The manager-tier reads for someone else's history live on\nthe separate Recognition Connect integration API\n(`/recognition/user_awards`, `/recognition/user_nominations`), which carry\ntheir own authorization.\n\n**One list per tab.** Recognition lives on two tables — program/manager\nawards and peer shout-outs. The web page renders them as stacked sections\nwith independent cursors; this endpoint merges them into ONE newest-first\nlist behind a single cursor, so the mobile screen is one scrolling card\nlist. Every row carries `type` (`award` / `recognition_post` /\n`nomination`) and shares an envelope, so one client component can render\nthem all. `awards` and `nominations` are single-type tabs — `awards` is\n`received` with the shout-outs removed, and is what the dashboard's\n\"My awards\" card pages into.\n\n**Everything is all-time**, because this is the lifetime history hub: the\nheadline totals, the tab badges and the tab lists are the same numbers and\nagree with one another. `/recognition/wrapped` is the separate\ncurrent-year recap, and the two intentionally differ for anyone with\nprior-year activity.\n\n**Where the numbers deliberately disagree.** `summary.total_given` counts\nFINALIZED gives only, so it matches the web tab badge and My Wrapped. The\nGiven LIST additionally carries gives still awaiting approval or\nmoderation — flagged `pending: true` with an `approval_state` — so a\njust-submitted give stays visible. That is why `meta.total_count` on the\nGiven tab equals `summary.total_given + summary.pending_given` and can\nexceed `total_given`. On Received and Nominations, `meta.total_count`\nequals the matching `summary` total exactly.\n\nAnonymous recognition never names the giver (`giver: null`,\n`anonymous: true`), and automated lifecycle awards (anniversaries,\nbirthdays) present as `System (Automated)` with a null id rather than\nleaking the system principal.\n\n**Every row carries the same `permissions` block the feed card and the\ndetail screen ship**, so one card component renders the same ⋯ menu\nwherever it meets a recognition and never an affordance the server would\nrefuse. Each flag is the canonical predicate the write endpoint itself\nenforces, so this is the one part of the payload where an admin's answer\ndiffers from an employee's. Four things behave differently here than on\nthe feed, all of them because of what this screen serves:\n\n* **`can_boost` is always `false`.** Every row is the caller's own give\n  or their own receipt, and boosting either is refused — which is also\n  why this endpoint reports no `boost` block at all.\n* **`can_comment` / `can_react` are `false` on a pending give**, not just\n  when the tenant switched them off. A recognition still awaiting\n  approval is not live, so neither request would be accepted — matching\n  the zero `engagement` the same row reports. Feed rows are always live,\n  so the feed can report the tenant switch alone.\n* **`can_delete` is `true` on a pending give the caller authored.** An\n  author may withdraw their own in-flight give; `can_edit` is `false` on\n  the same row, because only an active recognition is editable. The two\n  are not one gate.\n* **`can_delete` is withheld on award rows** (the same card-level choice\n  the feed makes — an award's delete is the admin revoke, confirmed on\n  the detail screen). Read `can_delete` from\n  `GET /recognitions/awards/{id}` before hiding a revoke entry.\n  `can_edit` is *not* withheld: a moderator may fix an award's message.\n\nA `nomination` row carries the block too, with every flag `false` — a\nnomination is not a recognition yet, and no edit, withdraw, share,\ncomment or reaction endpoint accepts one. The approve/reject decisions\nlive on the reviewer's own queue, never on the nominator's history row.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "tab",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "received",
                "given",
                "nominations",
                "awards"
              ],
              "default": "received"
            },
            "description": "Which record to page over. `nominations` requires award requests (Model A) to be enabled for the tenant; asking for it when it is off — like any unrecognized value — serves `received` rather than erroring, the same fallback the web page redirects that deep-link to. Read `meta.tab` for what was actually served and `meta.available_tabs` for what this tenant offers.\n`awards` is `received` with the peer shout-outs removed — every row is an `award`. It is the destination for the dashboard \"My awards\" card's own \"View all\": that card is the top 5 of THIS list, so the rows match and `meta.total_count` here equals `stats.recognition_received.awards` there. `summary` is unchanged on this tab (still the four all-time tiles), so a client can render the tile row from any tab."
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            },
            "description": "1-based. A missing, zero, negative or non-numeric value is page 1."
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            },
            "description": "Clamped to 1..50."
          }
        ],
        "responses": {
          "200": {
            "description": "My Recognition retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "reward_points",
                    "summary",
                    "meta"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "description": "The requested page, newest first. Mixed row types on Received/Given; `nomination` rows only on Nominations. Key off `type` — the award-only and post-only fields are present as null on the other types so the envelope never changes shape.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "type": {
                            "type": "string",
                            "enum": [
                              "award",
                              "recognition_post",
                              "nomination"
                            ],
                            "example": "award"
                          },
                          "id": {
                            "type": "integer",
                            "example": 481
                          },
                          "title": {
                            "type": "string",
                            "nullable": true,
                            "description": "The award or nomination title. Null for a peer shout-out, which has none.",
                            "example": "Above & Beyond"
                          },
                          "message": {
                            "type": "string",
                            "nullable": true,
                            "description": "The award description, the post body, or the nomination description.",
                            "example": "Nailed the migration under real pressure."
                          },
                          "points": {
                            "type": "integer",
                            "description": "Reward points, on EVERY row type — the figure a client's shared recognition card renders. An award stores dollars and converts (1 pt = 1¢); a peer post stores points directly; a nomination converts its approved value, or its requested value until one is approved. 0 when the row carries no value.",
                            "example": 250
                          },
                          "value": {
                            "type": "string",
                            "nullable": true,
                            "description": "Awards only — the stored DOLLAR figure, as a decimal string. Use `points` unless you specifically mean money.",
                            "example": "2.5"
                          },
                          "display_value": {
                            "type": "string",
                            "nullable": true,
                            "description": "Awards only — the label the web row shows (\"Priceless\" when unvalued).",
                            "example": "250 pts"
                          },
                          "recipient": {
                            "$ref": "#/components/schemas/RecognitionPerson"
                          },
                          "group_recipients": {
                            "type": "array",
                            "nullable": true,
                            "description": "Present only on a group give that collapsed into one row, naming every recipient.",
                            "items": {
                              "$ref": "#/components/schemas/RecognitionPerson"
                            }
                          },
                          "giver": {
                            "allOf": [
                              {
                                "$ref": "#/components/schemas/RecognitionPerson"
                              }
                            ],
                            "nullable": true,
                            "description": "Null when the recognition is anonymous. For an automated lifecycle award, `name` is \"System (Automated)\" and `id` is null."
                          },
                          "anonymous": {
                            "type": "boolean",
                            "example": false
                          },
                          "program": {
                            "type": "string",
                            "nullable": true,
                            "example": "Spot Awards"
                          },
                          "category": {
                            "type": "string",
                            "nullable": true,
                            "example": "Teamwork"
                          },
                          "company_value": {
                            "type": "string",
                            "nullable": true,
                            "example": "Ownership"
                          },
                          "visibility": {
                            "type": "string",
                            "nullable": true,
                            "description": "Posts only — public / department / team / private.",
                            "example": "public"
                          },
                          "tags": {
                            "type": "array",
                            "description": "Posts only. Always an array (never null).",
                            "items": {
                              "type": "string"
                            },
                            "example": [
                              "teamwork"
                            ]
                          },
                          "photo_url": {
                            "type": "string",
                            "nullable": true,
                            "description": "Absolute URL of the photo attached to this one recognition, already a processed rendition."
                          },
                          "award_art_url": {
                            "type": "string",
                            "nullable": true,
                            "description": "Absolute URL of the Asset Library award card this recognition was given with."
                          },
                          "certificate_url": {
                            "type": "string",
                            "nullable": true,
                            "description": "Awards only — the printable certificate the web row links to.",
                            "example": "/recognition/awards/481/card"
                          },
                          "engagement": {
                            "type": "object",
                            "description": "Reaction and comment counts. Always zero for a pending give — nothing can react to a recognition that is not live yet.",
                            "properties": {
                              "reactions_count": {
                                "type": "integer",
                                "example": 4
                              },
                              "comments_count": {
                                "type": "integer",
                                "example": 1
                              }
                            }
                          },
                          "permissions": {
                            "allOf": [
                              {
                                "$ref": "#/components/schemas/RecognitionPermissions"
                              }
                            ],
                            "description": "What this caller may do with THIS row — the same block the feed card and the detail screen ship, and the only part of the payload where an admin's answer differs from an employee's. Present on all three row types. `can_boost` is always false here, `can_comment` / `can_react` are false on a pending give, `can_delete` is withheld on award rows, and every flag is false on a `nomination` — see the endpoint description for why."
                          },
                          "pending": {
                            "type": "boolean",
                            "description": "A give still awaiting approval or moderation. Present on the Given tab only; excluded from `summary.total_given` but counted in `meta.total_count`.",
                            "example": false
                          },
                          "approval_state": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "posting",
                              "pending_approval",
                              "pending_review",
                              null
                            ],
                            "description": "Why a give is not live yet. `posting` is the automated content check (the normal path, no human is holding it); the other two need a reviewer to act."
                          },
                          "status": {
                            "type": "string",
                            "nullable": true,
                            "description": "Nominations only.",
                            "example": "pending"
                          },
                          "display_status": {
                            "type": "string",
                            "nullable": true,
                            "description": "Nominations only — the humanized status the web badge shows.",
                            "example": "Pending"
                          },
                          "status_color": {
                            "type": "string",
                            "nullable": true,
                            "description": "Nominations only — the hex the web badge is painted with.",
                            "example": "#ffc107"
                          },
                          "approval_level": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Nominations only — which approval step is pending (\"Level 1 of 2\").",
                            "example": 1
                          },
                          "approval_levels_required": {
                            "type": "integer",
                            "nullable": true,
                            "example": 2
                          },
                          "nominee": {
                            "allOf": [
                              {
                                "$ref": "#/components/schemas/RecognitionPerson"
                              }
                            ],
                            "nullable": true,
                            "description": "Nominations only."
                          },
                          "reviewer": {
                            "allOf": [
                              {
                                "$ref": "#/components/schemas/RecognitionPerson"
                              }
                            ],
                            "nullable": true,
                            "description": "Nominations only — null while unassigned."
                          },
                          "requested_value": {
                            "type": "string",
                            "nullable": true,
                            "description": "Nominations only — the dollar value asked for, as a decimal string. Use `requested_points` (or `points`) unless you specifically mean money.",
                            "example": "2.5"
                          },
                          "approved_value": {
                            "type": "string",
                            "nullable": true,
                            "description": "Nominations only — the dollar value granted, null until approved."
                          },
                          "requested_points": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Nominations only — `requested_value` in reward POINTS, the unit the row's `points` and every other figure in this API are denominated in. Null (not 0) when no value was asked for, so \"not specified\" is distinguishable from \"worth nothing\".",
                            "example": 250
                          },
                          "approved_points": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Nominations only — `approved_value` in reward points. Null until the nomination is approved.",
                            "example": 250
                          },
                          "reviewer_notes": {
                            "type": "string",
                            "nullable": true,
                            "description": "Nominations only — the reviewer's note, including a rejection reason."
                          },
                          "justification": {
                            "type": "string",
                            "nullable": true,
                            "description": "Nominations only."
                          },
                          "occurred_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When the recognition happened — awarded_at, published_at (created_at for a give not yet published), or the nomination's submitted_at. The field the list is sorted by."
                          },
                          "reviewed_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "description": "Nominations only."
                          }
                        }
                      }
                    },
                    "reward_points": {
                      "type": "object",
                      "description": "The caller's reward-points wallet. Reported whether or not the Company Store is reachable for this user — points are earned through recognition either way — so use `store_available` to decide whether to offer the redeem/history affordances, not whether to show the balance. Read-only: a GET never creates the wallet row.",
                      "properties": {
                        "store_available": {
                          "type": "boolean",
                          "description": "Whether the Company Store app is enabled for the tenant AND this user is inside its audience.",
                          "example": true
                        },
                        "points_balance": {
                          "type": "integer",
                          "description": "Points the caller can spend now.",
                          "example": 450
                        },
                        "pending_points": {
                          "type": "integer",
                          "description": "Earned but not yet settled (recognition still awaiting approval).",
                          "example": 25
                        },
                        "lifetime_points_earned": {
                          "type": "integer",
                          "example": 1200
                        },
                        "lifetime_points_spent": {
                          "type": "integer",
                          "example": 750
                        },
                        "expiring_points": {
                          "type": "integer",
                          "description": "Points that will have aged past the tenant's expiry window by the time the warning notification fires, so this number and that notification agree. 0 when the tenant has not configured expiry.",
                          "example": 150
                        },
                        "points_expiry_months": {
                          "type": "integer",
                          "description": "The tenant's expiry window in months; 0 means points never expire.",
                          "example": 12
                        }
                      }
                    },
                    "summary": {
                      "type": "object",
                      "description": "The four headline tiles / tab badges, byte-identical to the web page's own, plus `pending_given` and `total_awards`. All all-time, and identical on every tab.",
                      "properties": {
                        "total_received": {
                          "type": "integer",
                          "description": "Awards received + shout-outs received. Equals the Received tab's `meta.total_count`.",
                          "example": 12
                        },
                        "total_awards": {
                          "type": "integer",
                          "description": "The awards half of `total_received` — shout-outs excluded. Equals the Awards tab's `meta.total_count`, and the dashboard's `stats.recognition_received.awards`.",
                          "example": 4
                        },
                        "total_given": {
                          "type": "integer",
                          "description": "Awards given + shout-outs authored, FINALIZED only — matching the web badge and My Wrapped. Gives awaiting approval are counted in `pending_given` instead.",
                          "example": 28
                        },
                        "total_nominations": {
                          "type": "integer",
                          "description": "Nominations the caller submitted, every status. Equals the Nominations tab's `meta.total_count`.",
                          "example": 5
                        },
                        "pending_nominations": {
                          "type": "integer",
                          "description": "Of those, the ones still awaiting a decision.",
                          "example": 2
                        },
                        "pending_given": {
                          "type": "integer",
                          "description": "Gives still awaiting approval or moderation. Present in the Given LIST (flagged `pending: true`) but never in `total_given`.",
                          "example": 1
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "tab": {
                          "type": "string",
                          "enum": [
                            "received",
                            "given",
                            "nominations",
                            "awards"
                          ],
                          "description": "The tab actually served — compare with your request to detect a coercion.",
                          "example": "received"
                        },
                        "available_tabs": {
                          "type": "array",
                          "description": "The tabs this tenant offers. `nominations` is absent when award requests (Model A) are disabled; the other three are always present.",
                          "items": {
                            "type": "string"
                          },
                          "example": [
                            "received",
                            "given",
                            "nominations",
                            "awards"
                          ]
                        },
                        "current_page": {
                          "type": "integer",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "example": 20
                        },
                        "total_count": {
                          "type": "integer",
                          "description": "Rows in the whole list this tab pages over — the real total, not the size of the page.",
                          "example": 12
                        },
                        "total_pages": {
                          "type": "integer",
                          "example": 1
                        },
                        "has_next_page": {
                          "type": "boolean",
                          "example": false
                        },
                        "has_prev_page": {
                          "type": "boolean",
                          "example": false
                        },
                        "capabilities": {
                          "type": "object",
                          "properties": {
                            "can_give_recognition": {
                              "type": "boolean",
                              "description": "Whether the caller may give peer recognition at all — the SAME guard the submit path enforces. Hide the Give affordance when false; the POST would be rejected.",
                              "example": true
                            },
                            "award_requests_enabled": {
                              "type": "boolean",
                              "description": "Model A — gates the Nominations tab and the Nominate affordance.",
                              "example": true
                            }
                          }
                        }
                      }
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "The caller's unread notification count, for the app badge.",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Recognitions app is not enabled for the tenant, or this user is outside the app's audience (error code `access_denied`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/recognitions/wrapped": {
      "get": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Recognition Wrapped — the caller's personal year-in-recognition recap",
        "description": "The native-client mirror of the web **Recognition Wrapped** screen\n(`/recognition/wrapped`) — a Spotify-Wrapped-style recap of the caller's\nrecognition for one calendar year: how much they gave and received, the\ncompany values they were celebrated for, and the people they exchanged the\nmost recognition with. Both surfaces read the same service\n(`Recognition::WrappedService`) and the same year-resolution helpers, so\nthe numbers and the year range cannot drift between them.\n\n**Always self-scoped.** There is no `user_id` parameter and one sent anyway\nis ignored — this endpoint can only ever return the token holder's own\nrecap. The manager-tier reads for someone else's history live on the\nseparate Recognition Connect integration API, which carries its own\nauthorization.\n\n**Scoped to one calendar year by `created_at`.** `?year=` selects the\nrecap year; a missing, non-numeric or out-of-range value serves the current\nyear — the same clamp the web page applies to a hand-typed year. Read\n`meta.year` for what was actually served and `meta.available_years` for the\nrange this tenant offers (its creation year through the current year,\nnewest first).\n\n**Empty is a state, not an error.** A caller with no recognition this year\ngets `has_data: false` with zeroed tiles and empty lists (HTTP 200) — the\nclient renders the \"no recognition yet\" empty state rather than treating it\nas a failure.\n\nThe `top_champions` / `people_you_lifted_up` lists are capped at 3 and\nexclude the caller's own self-recognition; `celebrated_for` is capped at 3,\nmost-tagged first.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "year",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "The recap year. A missing, non-numeric or out-of-range value serves the current year (see `meta.year` / `meta.available_years`)."
          }
        ],
        "responses": {
          "200": {
            "description": "The recap retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "has_data",
                    "received",
                    "given",
                    "points_earned",
                    "celebrated_for",
                    "top_champions",
                    "people_you_lifted_up",
                    "highlights",
                    "meta"
                  ],
                  "properties": {
                    "has_data": {
                      "type": "boolean",
                      "description": "Whether the caller gave or received anything this year. When false the tiles are zero and the lists empty — render the empty state.",
                      "example": true
                    },
                    "received": {
                      "type": "object",
                      "description": "The \"Recognition received\" tile — total and its shout-out/award split.",
                      "required": [
                        "total",
                        "shout_outs",
                        "awards"
                      ],
                      "properties": {
                        "total": {
                          "type": "integer",
                          "example": 12
                        },
                        "shout_outs": {
                          "type": "integer",
                          "description": "Peer recognition posts received.",
                          "example": 8
                        },
                        "awards": {
                          "type": "integer",
                          "description": "Program/manager awards received.",
                          "example": 4
                        }
                      }
                    },
                    "given": {
                      "type": "object",
                      "description": "The \"Recognition given\" tile — total and its shout-out/award split.",
                      "required": [
                        "total",
                        "shout_outs",
                        "awards"
                      ],
                      "properties": {
                        "total": {
                          "type": "integer",
                          "example": 5
                        },
                        "shout_outs": {
                          "type": "integer",
                          "example": 3
                        },
                        "awards": {
                          "type": "integer",
                          "example": 2
                        }
                      }
                    },
                    "points_earned": {
                      "type": "integer",
                      "description": "Reward points earned from peer shout-outs this year (awards are not counted here).",
                      "example": 250
                    },
                    "celebrated_for": {
                      "type": "array",
                      "description": "Up to 3 company values the caller was recognized for, most-tagged first. `count` is the number of recognitions tagged to that value — the web card's progress bars are relative to the first (largest) row.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 17
                          },
                          "name": {
                            "type": "string",
                            "example": "Teamwork"
                          },
                          "count": {
                            "type": "integer",
                            "example": 6
                          }
                        }
                      }
                    },
                    "top_champions": {
                      "type": "array",
                      "description": "Up to 3 people who recognized the caller most this year. Never includes the caller.",
                      "items": {
                        "$ref": "#/components/schemas/WrappedPersonCount"
                      }
                    },
                    "people_you_lifted_up": {
                      "type": "array",
                      "description": "Up to 3 people the caller recognized most this year.",
                      "items": {
                        "$ref": "#/components/schemas/WrappedPersonCount"
                      }
                    },
                    "highlights": {
                      "type": "object",
                      "description": "The web \"Highlights\" card's lines. The first two are null when there's nothing to say.",
                      "required": [
                        "busiest_month",
                        "first_received_on",
                        "given_count",
                        "received_count"
                      ],
                      "properties": {
                        "busiest_month": {
                          "type": "string",
                          "nullable": true,
                          "description": "The calendar month the caller received the most (English month name), or null if nothing was received.",
                          "example": "March"
                        },
                        "first_received_on": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "description": "The date of the caller's first recognition this year (ISO 8601), or null.",
                          "example": "2026-03-04"
                        },
                        "given_count": {
                          "type": "integer",
                          "description": "Echoes `given.total`, so the card is self-contained.",
                          "example": 5
                        },
                        "received_count": {
                          "type": "integer",
                          "description": "Echoes `received.total`.",
                          "example": 12
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "year",
                        "available_years",
                        "capabilities"
                      ],
                      "properties": {
                        "year": {
                          "type": "integer",
                          "description": "The year actually served (after clamping).",
                          "example": 2026
                        },
                        "available_years": {
                          "type": "array",
                          "description": "The years a recap can be requested for — the tenant's creation year through the current year, newest first.",
                          "items": {
                            "type": "integer"
                          },
                          "example": [
                            2026,
                            2025,
                            2024
                          ]
                        },
                        "capabilities": {
                          "type": "object",
                          "properties": {
                            "can_give_recognition": {
                              "type": "boolean",
                              "description": "Whether the caller may give peer recognition — the SAME guard the submit path enforces. Hide the \"Give Recognition\" affordance when false.",
                              "example": true
                            }
                          }
                        }
                      }
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "The caller's unread notification count, for the app badge.",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Recognitions app is not enabled for the tenant, or this user is outside the app's audience (error code `access_denied`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/recognitions/feed": {
      "get": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Recognition feed (filtered, paginated)",
        "description": "The browsable recognition feed — the native-client mirror of the web\nfeed page. Both surfaces read the same query object, so the rows, their\norder, the visibility rules and the filters are identical.\n\n**What the caller sees** is decided entirely by visibility, never by a\nrole flag:\n\n* the tenant's **public awards** (non-public awards are never returned), and\n* **recognition posts** that are public, addressed to or written by the\n  caller, `department`-scoped and authored by someone in the caller's\n  department, or `team`-scoped and authored by a teammate.\n* `private` posts are returned **only** to their author and recipient.\n\nOnly recognition from the **last 30 days** is in scope — the feed is\n\"what's happening\", not an archive.\n\nA **group give** (one recognition sent to several people) collapses into\na single item whose `group_recipients` names everyone, rather than\nrepeating a near-identical card per recipient.\n\n**Roles are emergent.** An employee, a manager and an admin all run the\nsame query; they differ only in what it returns and in each item's\n`permissions` block. `my_team` resolves to a manager's direct reports,\nand to an employee's peers (their manager's reports).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "filter",
            "in": "query",
            "required": false,
            "description": "Which slice of the feed to return. Same five the web page offers;\nan unrecognised value falls back to `all` rather than erroring.\n  * `all`           — awards and posts together (default)\n  * `awards`        — public awards only\n  * `posts`         — peer recognition posts only\n  * `my_team`       — recognition given to or by the caller's team.\n                      Direct reports (+ self) for a manager; the\n                      caller's manager's reports (+ that manager)\n                      for an individual contributor. Falls back to\n                      the unfiltered feed when the caller has\n                      neither reports nor a manager.\n  * `my_department` — recognition given to or by anyone in the\n                      caller's department. Unfiltered when the\n                      caller has no department.\n",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "awards",
                "posts",
                "my_team",
                "my_department"
              ],
              "default": "all"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based page number. Values below 1 are treated as 1.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Items per page. Clamped to 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The requested page of the feed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RecognitionFeedItem"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/RecognitionFeedMeta"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "The caller's unread notification count, for the app badge.",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Recognitions app is not enabled for the tenant, or this user is outside the app's audience (error code `access_denied`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/recognitions/leaderboard": {
      "get": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Leaderboard — top recipients and top givers",
        "description": "The native-client mirror of the web **Recognition Leaderboard**\n(`/recognition/leaderboard`). Both surfaces read the same query object\n(`Recognition::LeaderboardQuery`), so the period windows, the tables the\ncounts are drawn from, the category basis switch, the tie-break and the\ncaller's own rank cannot drift between them.\n\n**Two boards, five rows each.** `top_recipients` ranks who was recognized\nmost; `top_givers` ranks who recognized others most. The web page shows 20\nrows per board and this endpoint shows 5 — same query, different depth,\nreported back as `meta.limit`.\n\n**No role branching.** The board is business-wide: an employee, a manager\nand a recognition admin receive exactly the same rows in the same order.\nThe only caller-specific parts of the payload are `my_standing`, the\n`is_me` flag on a row, and `meta.capabilities`. Access is gated the same\nway as every other endpoint in this namespace — the Recognitions app must\nbe enabled for the tenant AND the caller must be inside the app's\naudience, otherwise 403 `access_denied`.\n\n**Recognition lives on two tables** — formal awards and peer shout-outs.\nThe default \"All Categories\" view counts BOTH, so peer recognition is not\nundercounted. Peer shout-outs carry no category, so selecting a category\nnecessarily switches the basis to awards-only:\n`meta.counting_basis` becomes `awards_only` and `meta.basis_note` carries\nthe sentence to show the viewer. **Render that note** — without it a giver\nwhose recognition is mostly shout-outs appears to have fallen off the\nboard for no visible reason.\n\n**What is never counted:** revoked or expired awards and non-active posts\n(they are filtered out of every other surface, so counting them here would\ncredit recognition that was taken back), another tenant's recognition, and\n— on the givers board only — anonymous gives, since naming an anonymous\ngiver on a public board is exactly the disclosure they opted out of, and\nautomated lifecycle awards (anniversaries, birthdays, service\nmilestones), which are written by a system principal rather than by any\nperson. The recipient board is unaffected by either exclusion: it hides\nwho gave, not that someone was recognized, and a work-anniversary award\nis recognition its recipient genuinely received.\n\n**Two different meanings of \"rank\".** A row's `rank` is its 1-based\nPOSITION in the list — what the medallion renders (gold #1, silver #2,\nbronze #3, neutral from #4 down). `my_standing.*.rank` is competition\nstyle against the FULL board, so ties share a number and two people tied\nfor the lead are both rank 1. `my_standing.*.rank` is `null` — unranked,\nnot last — when the caller has no activity in the period; render nothing\nrather than a zero row. Use `my_standing.*.in_top` to decide whether to\npin a \"You — #47\" footer row, so it is never a duplicate of a row already\ndrawn above.\n\n**Filter values are coerced, not rejected.** An unrecognized `period`\nserves `month`; a `category` slug that is unknown, deleted or deactivated\nserves `all`. Always render `meta.period` / `meta.category` rather than\nechoing what was sent, or the filter chrome will claim a filter that was\nnever applied.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "period",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "week",
                "month",
                "quarter",
                "year"
              ],
              "default": "month"
            },
            "description": "Rolling window ending now — `month` is the last ~30 days, not the calendar month. An unrecognized value serves `month` rather than erroring; read `meta.period` for what was applied and `meta.period_start` / `meta.period_end` for the exact window."
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "all"
            },
            "description": "A RecognitionCategory slug from `meta.available_categories`, or `all`. A slug is matched across EVERY active category carrying it — the same slug can exist under more than one program — so picking \"Teamwork\" counts all of them. An unknown, deleted or deactivated slug serves `all`; read `meta.category` and `meta.counting_basis`."
          }
        ],
        "responses": {
          "200": {
            "description": "Leaderboard retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "top_recipients",
                    "top_givers",
                    "my_standing",
                    "meta"
                  ],
                  "properties": {
                    "top_recipients": {
                      "type": "array",
                      "description": "Who was recognized most in the window, highest first. At most `meta.limit` rows. Empty when nobody was recognized — render the empty state, not an error.",
                      "items": {
                        "$ref": "#/components/schemas/RecognitionLeaderboardRow"
                      }
                    },
                    "top_givers": {
                      "type": "array",
                      "description": "Who recognized others most in the window, highest first. Anonymous gives are excluded, so someone who only gave anonymously does not appear.",
                      "items": {
                        "$ref": "#/components/schemas/RecognitionLeaderboardRow"
                      }
                    },
                    "my_standing": {
                      "type": "object",
                      "description": "The caller's own position on each FULL board (not the visible slice), so a client can pin a \"You\" footer row when they rank below the top N.",
                      "required": [
                        "recipient",
                        "giver"
                      ],
                      "properties": {
                        "recipient": {
                          "$ref": "#/components/schemas/RecognitionLeaderboardStanding"
                        },
                        "giver": {
                          "$ref": "#/components/schemas/RecognitionLeaderboardStanding"
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "period",
                        "available_periods",
                        "period_start",
                        "period_end",
                        "category",
                        "available_categories",
                        "counting_basis",
                        "basis_note",
                        "givers_empty_reason",
                        "limit",
                        "capabilities"
                      ],
                      "properties": {
                        "period": {
                          "type": "string",
                          "enum": [
                            "week",
                            "month",
                            "quarter",
                            "year"
                          ],
                          "description": "The period ACTUALLY applied, after coercion.",
                          "example": "month"
                        },
                        "available_periods": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "example": [
                            "week",
                            "month",
                            "quarter",
                            "year"
                          ]
                        },
                        "period_start": {
                          "type": "string",
                          "format": "date-time",
                          "description": "Inclusive start of the rolling window.",
                          "example": "2026-07-13T17:49:10.082Z"
                        },
                        "period_end": {
                          "type": "string",
                          "format": "date-time",
                          "description": "Inclusive end of the window — the time the request was served.",
                          "example": "2026-08-13T17:49:10.084Z"
                        },
                        "category": {
                          "type": "string",
                          "description": "The category slug ACTUALLY applied, or `all`. An unknown or deactivated slug reports back as `all`.",
                          "example": "all"
                        },
                        "available_categories": {
                          "type": "array",
                          "description": "The filter options, `all` first, then the active categories of active programs. Deduplicated by slug — two programs can each define \"Teamwork\" and the filter covers both.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "slug": {
                                "type": "string",
                                "example": "teamwork"
                              },
                              "name": {
                                "type": "string",
                                "example": "Teamwork"
                              }
                            }
                          }
                        },
                        "counting_basis": {
                          "type": "string",
                          "enum": [
                            "awards_and_shout_outs",
                            "awards_only"
                          ],
                          "description": "Which tables produced the numbers. `awards_only` whenever a category is applied, because peer shout-outs carry no category.",
                          "example": "awards_and_shout_outs"
                        },
                        "basis_note": {
                          "type": "string",
                          "nullable": true,
                          "description": "The sentence to show the viewer alongside the filter when the basis narrowed. Null on the All Categories view. Render it — otherwise a giver's rank collapsing under a category filter reads as a bug.",
                          "example": "Counting formal awards only — peer shout-outs aren't category-tagged, so they're excluded from this view."
                        },
                        "givers_empty_reason": {
                          "type": "string",
                          "nullable": true,
                          "description": "Non-null only when `top_givers` is EMPTY because every give in this view was excluded (awarded by automation, or anonymous) while people were still recognized — a category such as Work Anniversary is awarded entirely by automation, so it has no human givers at all. Null whenever the board has rows, and null when nothing was recognized (a plain \"no activity yet\" state is honest there). Render this instead of a generic \"be the first to recognize someone\" prompt, which would urge an action nobody can take in that view.",
                          "example": "Recognition here was awarded automatically or given anonymously, so no one is credited on this board."
                        },
                        "limit": {
                          "type": "integer",
                          "description": "Maximum rows per board.",
                          "example": 5
                        },
                        "capabilities": {
                          "type": "object",
                          "properties": {
                            "can_give_recognition": {
                              "type": "boolean",
                              "description": "Whether THIS caller may give peer recognition — the same guard the submit path enforces, so a client never offers a Give button whose POST would be rejected."
                            }
                          }
                        }
                      }
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "The caller's unread notification count, for the app badge.",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Recognitions app is not enabled for the tenant, or this user is outside the app's audience (error code `access_denied`)."
          }
        }
      }
    },
    "/recognitions/leaderboard/categories": {
      "get": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Leaderboard category filter options",
        "description": "The **Filter by category** options for the Leaderboard, on their own so a\nclient can build the picker without pulling a board first (the mobile\nfilter sheet opens before any board is chosen).\n\nThis is the SAME list the board's own `meta.available_categories` is built\nfrom — active categories of active programs, `all` first, **deduplicated\nby slug** (a slug is unique per program, not per business, so two programs\ncan each define \"Teamwork\" and the filter covers both with one option).\nBecause both surfaces read the one query object, the picker can never\noffer a category the board would then reject.\n\nEach real option is enriched beyond the board's slug+name pair with the\n`color`, `icon` and `description` a colored filter chip needs, so the\npicker renders without a second round-trip. Those display fields fall back\nto a neutral swatch (`#95a5a6`) and a `star` icon when the tenant left\nthem blank, so a client never has to invent a default.\n\nThe list is **business-wide and identical for every role**, exactly like\nthe board it filters — the endpoint takes no parameters. Selecting any\ncategory narrows the board to formal awards only (peer shout-outs carry no\ncategory); `meta.counting_basis_note` is the sentence to surface next to\nthe picker so that narrowing never reads as a bug.\n\nOne query serves the whole list; every field is a column already on the\nloaded row, so there is no per-category lookup regardless of how many\ncategories the tenant has.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Filter options retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "categories",
                    "meta"
                  ],
                  "properties": {
                    "categories": {
                      "type": "array",
                      "description": "The picker options, `all` first, then one option per active category of an active program, ordered by the category's sort order then name, deduplicated by slug.",
                      "items": {
                        "type": "object",
                        "required": [
                          "slug",
                          "name",
                          "is_all"
                        ],
                        "properties": {
                          "slug": {
                            "type": "string",
                            "description": "The RecognitionCategory slug to pass as the board's `?category=` param, or `all` for the sentinel.",
                            "example": "teamwork"
                          },
                          "name": {
                            "type": "string",
                            "example": "Teamwork"
                          },
                          "is_all": {
                            "type": "boolean",
                            "description": "True only for the \"All Categories\" sentinel, so a client selects \"no filter\" without string-matching the slug.",
                            "example": false
                          },
                          "color": {
                            "type": "string",
                            "nullable": true,
                            "description": "Hex swatch for a filter chip. Null on the `all` sentinel; a neutral `#95a5a6` when the tenant left the category's color blank.",
                            "example": "#3498db"
                          },
                          "icon": {
                            "type": "string",
                            "nullable": true,
                            "description": "Icon name for the chip. Null on the `all` sentinel; a `star` fallback when the tenant left it blank.",
                            "example": "users"
                          },
                          "description": {
                            "type": "string",
                            "nullable": true,
                            "description": "The category's description, or null when blank.",
                            "example": "Working well together"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "count",
                        "counting_basis_note"
                      ],
                      "properties": {
                        "count": {
                          "type": "integer",
                          "description": "The number of REAL categories offered — the `all` sentinel is NOT counted, so this can be shown as \"5 categories\" honestly.",
                          "example": 4
                        },
                        "counting_basis_note": {
                          "type": "string",
                          "description": "The sentence to show next to the picker: selecting any category counts formal awards only, because peer shout-outs carry no category. Mirrors the board's `meta.basis_note`, shown BEFORE a category is applied rather than after.",
                          "example": "Selecting a category counts formal awards only — peer shout-outs aren't category-tagged, so they're excluded from a category view."
                        }
                      }
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "The caller's unread notification count, for the app badge.",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Recognitions app is not enabled for the tenant, or this user is outside the app's audience (error code `access_denied`)."
          }
        }
      }
    },
    "/recognitions/programs": {
      "get": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Recognition programs",
        "description": "The native-client mirror of the web Programs page. Every program, count,\npoints figure and CTA is produced by the same query object that backs the\nweb page (`Recognition::ProgramsQuery`), so the two surfaces cannot drift.\n\n**Two lists, because they answer different questions:**\n\n* `programs` — the ones a nomination can actually be filed under.\n  Paginated (12 per page, matching the web grid). This is the same basis\n  as the dashboard's `active_programs` card, so that card's own \"View All\"\n  can never land on a longer list than it counted.\n* `automatic_programs` — birthdays, work anniversaries and tenure\n  milestones, awarded by the lifecycle job. Nobody nominates in these, so\n  they are a separate section rather than padding the first one. Served\n  whole (a tenant has a handful at most) and every row reports\n  `can_nominate: false` with the automatic explanation.\n\nOnly **active** programs **inside their start/end window** are listed. A\ndraft, retired, not-yet-open or closed program is absent from both lists.\n\n**The role story is `can_nominate`, and nothing else.** There is no\nrole-scoped hiding: an employee, a manager and an admin all receive the\nsame programs. What differs is whether each card offers the CTA, and that\nanswer comes from the program's own eligibility rule — the SAME predicate\nthe nomination submit path enforces, so a client is never offered a button\nwhose POST would be rejected:\n\n* a `peer_to_peer` program is open to everyone;\n* a `manager_to_employee` program is open to **managers only**, both ways\n  — an admin who is not a manager is blocked too;\n* an `achievement_based` program with nominator `eligibility_criteria`\n  (tenure, department, location, role) answers per profile;\n* a `milestone` program is never nominatable by anyone.\n\nWhen `can_nominate` is false, `nomination_block_reason` says why in the\nviewer's own words. **Render it** — a disabled button with no reason and\nno next step is a dead end, and the web card shows the same sentence.\n\n`reward_points` presents the program's monthly recognition budget in the\nPOINTS employees see everywhere else in this app (the economy is stored in\ndollars; 1 point = 1¢, at the tenant's `points_per_dollar` store setting).\nThe dollar figures ride along for consumers that report currency.\n\nRequires ad-hoc award requests (\"Model A\") to be enabled for the tenant —\nthe same gate that hides the web Programs tab. A tenant with it off gets\n403 `feature_disabled` rather than a list of programs nobody there can\nnominate in.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            },
            "description": "1-based page number. Paginates `programs` only — `automatic_programs` is served whole on every page."
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 12
            },
            "description": "Cards per page, clamped to 50. Defaults to 12, matching the web grid, so the two surfaces page identically."
          }
        ],
        "responses": {
          "200": {
            "description": "Programs retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "programs",
                    "automatic_programs",
                    "meta"
                  ],
                  "properties": {
                    "programs": {
                      "type": "array",
                      "description": "Programs this viewer can browse and (subject to `can_nominate`) nominate in. Ordered by name. Paginated.",
                      "items": {
                        "$ref": "#/components/schemas/RecognitionProgramCard"
                      }
                    },
                    "automatic_programs": {
                      "type": "array",
                      "description": "System-awarded milestone programs — every row has `is_automatic: true`, `can_nominate: false` and `my_nominations: 0`. Not paginated.",
                      "items": {
                        "$ref": "#/components/schemas/RecognitionProgramCard"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "current_page": {
                          "type": "integer",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "example": 12
                        },
                        "total_count": {
                          "type": "integer",
                          "description": "Nominatable programs across all pages. Describes `programs` only; `automatic_programs` is served whole, so its length IS its total.",
                          "example": 3
                        },
                        "total_pages": {
                          "type": "integer",
                          "example": 1
                        },
                        "has_next_page": {
                          "type": "boolean",
                          "example": false
                        },
                        "has_prev_page": {
                          "type": "boolean",
                          "example": false
                        },
                        "automatic_programs_count": {
                          "type": "integer",
                          "description": "Length of `automatic_programs`.",
                          "example": 2
                        },
                        "capabilities": {
                          "type": "object",
                          "properties": {
                            "can_nominate_any": {
                              "type": "boolean",
                              "description": "Whether this viewer can nominate in ANY program on this page — the web header's \"Nominate Someone\" button. False means the nominee picker would be empty for them, so hide the entry point.",
                              "example": true
                            },
                            "can_give_recognition": {
                              "type": "boolean",
                              "description": "Whether this viewer may give peer recognition at all — the same guard the submit path enforces.",
                              "example": true
                            },
                            "award_requests_enabled": {
                              "type": "boolean",
                              "description": "Model A. Always true in a 200 response — the endpoint 403s when it is off — and reported so a client can cache one settings shape.",
                              "example": true
                            },
                            "award_cycles_enabled": {
                              "type": "boolean",
                              "description": "Model B (time-boxed award cycles). Independent of this endpoint; reported so a client knows whether the Award Cycles surface exists for this tenant.",
                              "example": false
                            }
                          }
                        }
                      }
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "The caller's unread notification count, for the app badge.",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Either the Recognitions app is not enabled for the tenant / this user is outside the app's audience (error code `access_denied`), or ad-hoc award requests are disabled for the tenant, which also hides the web Programs tab (error code `feature_disabled`)."
          }
        }
      }
    },
    "/recognitions/team": {
      "get": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Team Recognition",
        "description": "The native-client mirror of the web **Team Recognition** page. Every\nnumber, list and scope is produced by the same query object that backs\nthe web page (`Recognition::TeamStats`), so the two surfaces cannot drift\non what a number means, who is on \"my team\", or who may see a row.\n\n**Reviewer-only.** The web page is gated on the canonical\n`User#recognition_reviewer?` — a business admin, a Recognitions app\nadmin, or anyone with direct reports — and redirects everyone else. This\nendpoint refuses with **403 `forbidden`** rather than returning an\nall-zero payload, which a client would render as \"your team has never\nbeen recognized\". Check `viewer.is_reviewer` on the dashboard endpoint\nbefore offering the Team tab.\n\n**Manager vs admin.** `viewer.is_recognition_admin` changes the reach of\ntwo sections, and the client should label them accordingly:\n\n* `pending_approvals` — an admin's queue spans the whole tenant; a\n  manager's is limited to nominations they may actually review (assigned\n  to them, unassigned, or about one of their own reports) plus the posts\n  routed to them.\n* `upcoming_anniversaries` — an admin sees the whole active workforce; a\n  manager sees only their own team.\n\n`team_members` is always the viewer's OWN reports, for both personas —\nwhich is why an admin with nobody reporting to them gets an empty roster\nbeside a full approvals queue. The web page says exactly that in its\nempty state.\n\n**Counting rules**, applied identically to every tile and list:\n\n* Recognition lives on two tables — formal **awards** and peer\n  **shout-outs** — and every received/given number rolls up both. Every\n  shout-out counts, not only point-bearing ones: a tenant that doesn't\n  use redeemable rewards collects no points on the give form at all.\n* Revoked awards are excluded everywhere (their store points have been\n  clawed back).\n* Private posts are excluded everywhere **except** `needs_recognition`.\n  That panel prints no content, and someone recognized privately HAS been\n  recognized — flagging them would nag the manager into a duplicate.\n\nAll values that represent money are reported in **reward points**;\nstorage is in dollars and the conversion happens server-side, so a client\nnever needs the tenant's points-per-dollar rate.\n",
        "parameters": [
          {
            "name": "team_scope",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "direct",
                "all"
              ],
              "default": "direct"
            },
            "description": "`direct` (default) lists the viewer's direct reports; `all` widens to their whole org subtree. A **request, not the answer** — when no direct report has reports of their own, `all` is the same set, so the server forces the response back to `direct` and reports `scope.has_indirect_reports: false`. Read `scope.current`, never the value you sent, and only offer the toggle when `scope.has_indirect_reports` is true."
          }
        ],
        "responses": {
          "200": {
            "description": "The Team screen",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "team": {
                      "type": "object",
                      "properties": {
                        "viewer": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 412
                            },
                            "name": {
                              "type": "string",
                              "example": "Maya Chen"
                            },
                            "image": {
                              "type": "string",
                              "nullable": true,
                              "description": "Absolute avatar URL, or null when it can't be resolved."
                            },
                            "is_reviewer": {
                              "type": "boolean",
                              "description": "Always true in a 200 — the gate refuses everyone else. Sent so the payload is self-describing.",
                              "example": true
                            },
                            "is_recognition_admin": {
                              "type": "boolean",
                              "description": "Business admin-or-above, or a Recognitions app admin. Widens `pending_approvals` to the tenant and `upcoming_anniversaries` to the whole workforce.",
                              "example": false
                            },
                            "can_give_recognition": {
                              "type": "boolean",
                              "description": "Whether the tenant lets THIS user give peer recognition — the same guard the submit path enforces, so a Give button whose POST would be rejected is never offered.",
                              "example": true
                            }
                          }
                        },
                        "features": {
                          "type": "object",
                          "description": "Tenant toggles, mirrored (not re-derived) from the same settings that gate the web nav and views, so a client can hide exactly the surfaces the web page hides.",
                          "properties": {
                            "award_requests_enabled": {
                              "type": "boolean",
                              "description": "Model A — ad-hoc award requests. Defaults on.",
                              "example": true
                            },
                            "award_cycles_enabled": {
                              "type": "boolean",
                              "description": "Model B — time-boxed award cycles. Defaults off.",
                              "example": false
                            },
                            "quick_award_enabled": {
                              "type": "boolean",
                              "description": "Instant manager award, no approval step. Gates the Recognize / Celebrate button the web page hangs off every roster, gap and anniversary row.",
                              "example": true
                            },
                            "peer_points_enabled": {
                              "type": "boolean",
                              "description": "Whether a peer allowance pool exists at all. When false, `my_giving` carries program budgets only.",
                              "example": false
                            }
                          }
                        },
                        "scope": {
                          "type": "object",
                          "properties": {
                            "current": {
                              "type": "string",
                              "enum": [
                                "direct",
                                "all"
                              ],
                              "description": "The scope actually in force — read this, not the request.",
                              "example": "direct"
                            },
                            "has_indirect_reports": {
                              "type": "boolean",
                              "description": "Whether any direct report has reports of their own. False → the `all` toggle is meaningless; don't render it.",
                              "example": false
                            },
                            "label": {
                              "type": "string",
                              "description": "Ready-to-render label matching `current`.",
                              "example": "Direct reports"
                            }
                          }
                        },
                        "stats": {
                          "type": "object",
                          "description": "The four tiles the screen leads with. Received and Given carry the awards-vs-shout-outs split alongside the total (the total IS the split), so a client can render the sub-line without a second request.",
                          "properties": {
                            "recognition_received": {
                              "$ref": "#/components/schemas/RecognitionTeamCountTile"
                            },
                            "recognition_given": {
                              "$ref": "#/components/schemas/RecognitionTeamCountTile"
                            },
                            "total_value": {
                              "type": "object",
                              "properties": {
                                "points": {
                                  "type": "integer",
                                  "description": "The team's total recognition value in reward points, converted from the dollars awards are stored in.",
                                  "example": 4250
                                }
                              }
                            },
                            "team_members": {
                              "type": "object",
                              "properties": {
                                "total": {
                                  "type": "integer",
                                  "description": "Always equals the length of `team_members` — the tile and the roster count the same set.",
                                  "example": 6
                                },
                                "scope": {
                                  "type": "string",
                                  "enum": [
                                    "direct",
                                    "all"
                                  ],
                                  "description": "Which scope produced this count.",
                                  "example": "direct"
                                }
                              }
                            }
                          }
                        },
                        "my_giving": {
                          "type": "array",
                          "description": "One row per pool the viewer can give from — the monthly peer allowance and each award-program budget they draw on. Empty when the tenant has configured neither.",
                          "items": {
                            "$ref": "#/components/schemas/RecognitionTeamGivingPool"
                          }
                        },
                        "pending_approvals": {
                          "type": "object",
                          "description": "The viewer's approval queue. Recognition stays hidden from the recipient until a reviewer decides, so a reviewer who never learns they have a queue silently blocks it.",
                          "properties": {
                            "total": {
                              "type": "integer",
                              "description": "`nominations_count + posts_count` — the badge number. Counting nominations alone left reviewers reading \"Pending (0)\" while posts sat unapproved.",
                              "example": 3
                            },
                            "nominations_count": {
                              "type": "integer",
                              "description": "The FULL queue size, computed before the preview limit — never capped at the number of rows below.",
                              "example": 2
                            },
                            "posts_count": {
                              "type": "integer",
                              "example": 1
                            },
                            "nominations": {
                              "type": "array",
                              "description": "The first 5 nominations. Pending POSTS are deliberately not previewed: an unapproved post is unpublished recognition, so its body is shown nowhere until it is approved — send the reviewer to the approvals screen for those.",
                              "items": {
                                "$ref": "#/components/schemas/RecognitionTeamPendingNomination"
                              }
                            }
                          }
                        },
                        "needs_recognition": {
                          "type": "object",
                          "description": "The fairness signal — who is going unrecognized.",
                          "properties": {
                            "count": {
                              "type": "integer",
                              "description": "The FULL number flagged. `members` is capped at 8, so render \"+N more\" from the difference rather than implying the list is complete.",
                              "example": 3
                            },
                            "threshold_days": {
                              "type": "integer",
                              "description": "A member is flagged after this long with nothing at all.",
                              "example": 30
                            },
                            "members": {
                              "type": "array",
                              "description": "Never-recognized first, then longest gap first. At most 8 rows.",
                              "items": {
                                "$ref": "#/components/schemas/RecognitionTeamGapRow"
                              }
                            }
                          }
                        },
                        "upcoming_anniversaries": {
                          "type": "object",
                          "properties": {
                            "window_days": {
                              "type": "integer",
                              "description": "How far ahead the list looks.",
                              "example": 30
                            },
                            "entries": {
                              "type": "array",
                              "description": "Soonest first, at most 5.",
                              "items": {
                                "$ref": "#/components/schemas/RecognitionTeamAnniversary"
                              }
                            }
                          }
                        },
                        "team_highlights": {
                          "type": "object",
                          "description": "Who stands out this period. Both rows are **null** when the team has no recognition at all — render nothing rather than a zero-count row (the web prints an em dash).",
                          "properties": {
                            "most_recognized": {
                              "$ref": "#/components/schemas/RecognitionTeamHighlight"
                            },
                            "most_recognition_given": {
                              "$ref": "#/components/schemas/RecognitionTeamHighlight"
                            }
                          }
                        },
                        "recent_team_recognition": {
                          "type": "array",
                          "description": "The merged award + shout-out activity stream for this team, newest first, at most 5 rows.",
                          "items": {
                            "$ref": "#/components/schemas/RecognitionTeamActivityRow"
                          }
                        },
                        "team_members": {
                          "type": "array",
                          "description": "The roster the web renders as a table, each row carrying that person's received / given / this-month counts. The counts come from the same grouped queries the tiles use, so the table costs no extra query. Always the viewer's OWN reports — empty for an admin with nobody reporting to them.",
                          "items": {
                            "$ref": "#/components/schemas/RecognitionTeamMemberRow"
                          }
                        }
                      }
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "The caller's unread notification count, for the app badge.",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Either the Recognitions app is not enabled for the tenant / this user is outside the app's audience (error code `access_denied`), or the caller is not a reviewer — no direct reports and not a Recognitions admin (error code `forbidden`), which is the API's answer to the web page's redirect."
          }
        }
      }
    },
    "/recognitions/team/members/{id}": {
      "get": {
        "tags": [
          "Recognitions"
        ],
        "summary": "One team member's recognition profile",
        "description": "The native-client mirror of the web **team member** screen — the page a\nrow of the Team roster opens (`/recognition/manager/team_member/{id}`).\nEverything a manager can see about one person's recognition: five tiles\nand three lists.\n\nEvery number and list is produced by the same query object that backs the\nweb page (`Recognition::TeamMemberProfile`), so the two surfaces cannot\ndrift on who may open a profile, what a tile counts, or which rows are\nsafe to show the subject's manager.\n\n**Two gates, exactly like the web page.**\n\n1. The SURFACE is reviewer-only — a business admin, a Recognitions app\n   admin, or anyone with direct reports (`User#recognition_reviewer?`).\n   A plain employee gets **403 `forbidden`**. Check\n   `viewer.is_reviewer` on `/recognitions/dashboard` before offering the\n   Team tab at all.\n2. The MEMBER is then checked individually. A recognition admin reaches\n   anyone in the tenant; a manager reaches only their own direct reports\n   or someone deeper in their org subtree. Anyone else is **403\n   `forbidden`** with the page's own wording.\n\nA user outside this tenant, or a nonexistent id, is **404** — indistinct\nfrom each other, so this endpoint can't be used to probe another tenant's\nuser ids. Note that **nobody is on their own team**: a manager asking for\ntheir own id gets 403. `/recognitions/my_recognition` is that screen.\n\n**Counting rules**, applied identically to every tile and every list:\n\n* Recognition lives on two tables — formal **awards** and peer\n  **shout-outs**. The two \"This Month\" tiles roll up BOTH, because the\n  \"This Month\" column on the roster that links here does; the awards /\n  shout-out split is published alongside each total so a client can\n  render the sub-line without a second request.\n* **Revoked awards are excluded everywhere** — their store points have\n  been clawed back, so they are gone from the leaderboard and the\n  member's own record too.\n* **Private posts are excluded everywhere**, from the counts as well as\n  the lists. The give form promises a private post is \"Only you and the\n  recipient\", and this screen renders post bodies to the recipient's\n  MANAGER — so counting one the list can never show would disclose that\n  it exists.\n* Posts that are not active (pending or rejected moderation) are\n  excluded: they are unpublished recognition.\n\n**Not paginated.** Each list returns the latest 20 rows, newest first,\nalongside the full `total_count` and a `has_more` flag — which is what\nthe web prints as \"Showing latest 20 of N\". For deeper history use the\nbrowsable, paginated `/recognitions/feed`.\n\nAll values that represent money are reported in **reward points**;\nstorage is in dollars and the conversion happens server-side, so a client\nnever needs the tenant's points-per-dollar rate.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The team member's user id — the `user.id` of a `team_members` row from `/recognitions/team`."
          }
        ],
        "responses": {
          "200": {
            "description": "The team member's recognition profile",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "team_member": {
                      "type": "object",
                      "properties": {
                        "member": {
                          "$ref": "#/components/schemas/RecognitionProfileSubject"
                        },
                        "viewer": {
                          "type": "object",
                          "description": "The header's affordances, as the server will actually answer them.",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 412
                            },
                            "name": {
                              "type": "string",
                              "example": "Maya Chen"
                            },
                            "image": {
                              "type": "string",
                              "nullable": true,
                              "description": "Absolute avatar URL, or null when it can't be resolved."
                            },
                            "is_recognition_admin": {
                              "type": "boolean",
                              "description": "Business admin-or-above, or a Recognitions app admin. An admin viewing somebody else's report is not looking at \"my team\" — label the two differently.",
                              "example": false
                            },
                            "can_revoke_awards": {
                              "type": "boolean",
                              "description": "Whether to offer **Revoke** on award rows. Clawback is admin-only even though managers may open this screen, so this is false for a manager. Uniform across both award lists rather than per row: every row here is an active award, and the control does not vary by which one it points at.",
                              "example": false
                            },
                            "can_give_recognition": {
                              "type": "boolean",
                              "description": "The \"Recognize\" button. The same guard the give endpoint enforces, so a button whose POST would be rejected is never offered.",
                              "example": true
                            }
                          }
                        },
                        "features": {
                          "type": "object",
                          "properties": {
                            "quick_award_enabled": {
                              "type": "boolean",
                              "description": "The \"Quick Award\" button — instant manager award, no approval step. The tenant switch alone; this endpoint is already reviewer-gated, which is the other half of the write path's guard.",
                              "example": true
                            }
                          }
                        },
                        "stats": {
                          "type": "object",
                          "description": "The five tiles.",
                          "properties": {
                            "awards_received": {
                              "type": "integer",
                              "description": "All-time active awards received.",
                              "example": 5
                            },
                            "awards_given": {
                              "type": "integer",
                              "description": "All-time active awards this member gave.",
                              "example": 3
                            },
                            "posts_received": {
                              "type": "integer",
                              "description": "All-time active, non-private shout-outs received — the same set the `recognition_posts_received` list draws from.",
                              "example": 15
                            },
                            "this_month_received": {
                              "$ref": "#/components/schemas/RecognitionProfileMonthTile"
                            },
                            "this_month_given": {
                              "$ref": "#/components/schemas/RecognitionProfileMonthTile"
                            }
                          }
                        },
                        "awards_received": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/RecognitionProfileSection"
                            }
                          ],
                          "description": "Formal awards this member received, newest first."
                        },
                        "recognition_posts_received": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/RecognitionProfileSection"
                            }
                          ],
                          "description": "Peer shout-outs this member received, newest first. Private ones are never included."
                        },
                        "awards_given": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/RecognitionProfileSection"
                            }
                          ],
                          "description": "Awards this member gave, newest first — each row names the RECIPIENT."
                        }
                      }
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "The caller's unread notification count, for the app badge.",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "One of three refusals, distinguished by `error.code`: the Recognitions app is not enabled for the tenant or this user is outside the app's audience (`access_denied`); the caller is not a reviewer — no direct reports and not a Recognitions admin (`forbidden`); or the caller is a reviewer but this member is not on their team (`forbidden`, message \"You can only view recognition for people on your team\"), which is the API's answer to the web page's redirect."
          },
          "404": {
            "description": "No such member in this tenant. Deliberately indistinguishable from a nonexistent id, so the endpoint can't be used to enumerate another tenant's users."
          }
        }
      }
    },
    "/recognitions/anniversary_roster": {
      "get": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Anniversary Roster — the full work-anniversary list, filtered",
        "description": "The native-client mirror of the web **Anniversary Roster**, which is where\nthe Team screen's \"Upcoming anniversaries\" card sends **View all**. Both\nsurfaces read the same query object\n(`Recognition::AnniversaryRosterQuery`), so the filters, the reach and\nevery tile are the same code on both.\n\n**This is the roster; `/recognitions/team` carries a teaser.** That\nendpoint's `upcoming_anniversaries` is a fixed 30-day window, five rows,\nno filters. This one takes the viewer's own date range and milestone-year\nfilter, reports the four summary tiles, and pages.\n\n**Reviewer-only.** Gated on the canonical `User#recognition_reviewer?` —\na business admin, a Recognitions app admin, or anyone with direct reports\n— exactly like the web page, which redirects everyone else. This endpoint\nanswers **403 `forbidden`** rather than an empty roster, which a client\nwould render as \"nobody has an anniversary\".\n\n**Reach follows the persona**, and `scope.reach` says which you got:\n\n* `workforce` — a recognition admin sees every active employee, because\n  they own the milestone program.\n* `team` — anyone else sees their DIRECT reports. There is no\n  \"all reports\" toggle here, because the web page has none.\n\nOnly people with a hire date are listed, and only from their FIRST\nanniversary onward — a hire from last month has nothing to celebrate yet.\nA Feb 29 hire is observed on Feb 28 in non-leap years.\n\n**The tiles describe the FULL filtered roster, never the page** — a count\nthat only covered page 1 would contradict the list beside it. `upcoming`\ntherefore always equals `meta.total_count`.\n\n**Filters are refused, not coerced** — unlike `/recognitions/leaderboard`,\nwhere an unknown period sensibly falls back to `month`. There is no\nsensible fallback for a date: quietly serving the default window for\n`from_date=last-tuesday`, or an empty list for a backwards range, both\nread to the user as \"nobody has an anniversary\" when the truth is \"your\nfilter didn't arrive\". Each of these is **400 `invalid_parameter`**:\n\n* a date that can't be parsed,\n* `to_date` before `from_date`,\n* a range wider than 365 days (past a year every calendar day is in the\n  window, so the answer can't differ and the scan can't be narrowed),\n* a `milestone_years` with no readable year in it.\n\nRender `filters` rather than echoing what you sent: blank params resolve\nto the default window server-side.\n",
        "parameters": [
          {
            "name": "from_date",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Start of the window (inclusive). Defaults to **today**. `start_date` is accepted as an alias, so a roster URL copied out of the web app works unchanged."
          },
          {
            "name": "to_date",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "End of the window (inclusive). Defaults to **today + 90 days**, the web page's own window. `end_date` is accepted as an alias. Must be on or after `from_date`, and no more than 365 days after it."
          },
          {
            "name": "milestone_years",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "example": "5,10,15"
            },
            "description": "Comma-separated service-year counts — only people completing exactly these years are listed. Omit for every year. Whitespace and duplicates are tolerated; a value with no readable year in it is a 400 rather than a silent unfiltered list. `filters.milestone_year_options` carries the presets the web filter offers."
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 100
            },
            "description": "Defaults to 50, the web table's page size. Clamped to 100."
          }
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The Anniversary Roster screen",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "anniversary_roster": {
                      "type": "object",
                      "properties": {
                        "viewer": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 412
                            },
                            "name": {
                              "type": "string",
                              "example": "Maya Chen"
                            },
                            "image": {
                              "type": "string",
                              "nullable": true,
                              "description": "Absolute avatar URL, or null when it can't be resolved."
                            },
                            "is_reviewer": {
                              "type": "boolean",
                              "description": "Always true in a 200 — the gate refuses everyone else. Sent so the payload is self-describing.",
                              "example": true
                            },
                            "is_recognition_admin": {
                              "type": "boolean",
                              "description": "Business admin-or-above, or a Recognitions app admin. Widens the roster from this manager's reports to the whole active workforce.",
                              "example": false
                            },
                            "can_give_recognition": {
                              "type": "boolean",
                              "example": true
                            }
                          }
                        },
                        "features": {
                          "type": "object",
                          "properties": {
                            "quick_award_enabled": {
                              "type": "boolean",
                              "description": "Instant manager award, no approval step. Gates the Recognize / Celebrate button on every row — when false, no row reports `can_recognize: true`.",
                              "example": true
                            }
                          }
                        },
                        "scope": {
                          "type": "object",
                          "description": "Whose anniversaries this roster covers.",
                          "properties": {
                            "reach": {
                              "type": "string",
                              "enum": [
                                "workforce",
                                "team"
                              ],
                              "description": "`workforce` for a recognition admin, `team` (the viewer's direct reports) for anyone else. Label the list accordingly — the same roster means different things to the two personas.",
                              "example": "team"
                            },
                            "label": {
                              "type": "string",
                              "example": "Your direct reports"
                            }
                          }
                        },
                        "filters": {
                          "type": "object",
                          "description": "What was actually applied, plus what the picker should offer.",
                          "properties": {
                            "from_date": {
                              "type": "string",
                              "format": "date"
                            },
                            "to_date": {
                              "type": "string",
                              "format": "date"
                            },
                            "window_days": {
                              "type": "integer",
                              "description": "Days between from_date and to_date.",
                              "example": 90
                            },
                            "max_window_days": {
                              "type": "integer",
                              "description": "The widest range this endpoint serves. Asking for more is a 400.",
                              "example": 365
                            },
                            "milestone_years": {
                              "type": "array",
                              "nullable": true,
                              "description": "The years actually filtered on, parsed and sorted. **Null** means every year, which is the default.",
                              "items": {
                                "type": "integer"
                              },
                              "example": [
                                5,
                                10
                              ]
                            },
                            "is_filtered": {
                              "type": "boolean",
                              "description": "Whether anything is narrowed — i.e. whether a \"Clear filters\" affordance is worth showing. Matches the web empty state's own condition.",
                              "example": false
                            },
                            "defaults": {
                              "type": "object",
                              "description": "The window served when no dates are sent.",
                              "properties": {
                                "from_date": {
                                  "type": "string",
                                  "format": "date"
                                },
                                "to_date": {
                                  "type": "string",
                                  "format": "date"
                                },
                                "window_days": {
                                  "type": "integer",
                                  "example": 90
                                }
                              }
                            },
                            "milestone_year_options": {
                              "type": "array",
                              "description": "The presets the web filter's select offers, so a native picker renders the same choices instead of maintaining a second list that can drift. `years: null` is \"All Years\".",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "label": {
                                    "type": "string",
                                    "example": "Mid (5, 10, 15)"
                                  },
                                  "years": {
                                    "type": "array",
                                    "nullable": true,
                                    "items": {
                                      "type": "integer"
                                    },
                                    "example": [
                                      5,
                                      10,
                                      15
                                    ]
                                  }
                                }
                              }
                            }
                          }
                        },
                        "stats": {
                          "type": "object",
                          "description": "The web page's four tiles, in its order. Every one describes the FULL filtered roster, never the page — `upcoming` always equals `meta.total_count`.",
                          "properties": {
                            "upcoming": {
                              "type": "integer",
                              "description": "Everyone in the window, after filtering.",
                              "example": 24
                            },
                            "this_month": {
                              "type": "integer",
                              "description": "How many fall in the CURRENT calendar month — not the first month of the window, so a range starting next March doesn't report March as \"this month\".",
                              "example": 6
                            },
                            "milestone_years": {
                              "type": "array",
                              "description": "The distinct service-year counts present in the roster, ascending — the LIST, not its size. (\"4\" beside \"Upcoming 120\" read as \"only 4 of them are milestones\".) Ships whole.",
                              "items": {
                                "type": "integer"
                              },
                              "example": [
                                1,
                                3,
                                5,
                                10
                              ]
                            },
                            "milestone_years_display_limit": {
                              "type": "integer",
                              "description": "Where the web tile collapses the rest into \"+N more\". Guidance, not a rule.",
                              "example": 8
                            },
                            "recognized": {
                              "type": "integer",
                              "description": "How many already received their years-of-service award this year.",
                              "example": 3
                            }
                          }
                        },
                        "entries": {
                          "type": "array",
                          "description": "One page of the roster, soonest anniversary first.",
                          "items": {
                            "$ref": "#/components/schemas/RecognitionAnniversaryRosterRow"
                          }
                        },
                        "meta": {
                          "type": "object",
                          "description": "Page envelope. Counts describe the whole filtered roster.",
                          "properties": {
                            "current_page": {
                              "type": "integer",
                              "example": 1
                            },
                            "per_page": {
                              "type": "integer",
                              "example": 50
                            },
                            "total_count": {
                              "type": "integer",
                              "example": 24
                            },
                            "total_pages": {
                              "type": "integer",
                              "example": 1
                            },
                            "has_next_page": {
                              "type": "boolean",
                              "example": false
                            },
                            "has_prev_page": {
                              "type": "boolean",
                              "example": false
                            }
                          }
                        }
                      }
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "The caller's unread notification count, for the app badge.",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A filter that couldn't be read (error code `invalid_parameter`): an unparseable date, `to_date` before `from_date`, a range wider than 365 days, or a `milestone_years` with no readable year. The message names the offending parameter — show it rather than a generic failure, since the fix is the user's to make."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Either the Recognitions app is not enabled for the tenant / this user is outside the app's audience (error code `access_denied`), or the caller is not a reviewer — no direct reports and not a Recognitions admin (error code `forbidden`), which is the API's answer to the web page's redirect."
          }
        }
      }
    },
    "/recognitions/award_cycles": {
      "get": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Award Cycles — open now, opening soon and recent winners",
        "description": "The native-client mirror of the web **Award Cycles** page\n(`/recognition/award-cycles`) — the time-boxed award cycles (Model B, the\n\"Mango Champions\" pattern) a viewer can nominate into, plus what opens\nnext and who recently won. Both surfaces read the same query object\n(`Recognition::AwardCyclesQuery`), so the section a cycle lands in, the\ncommittee review rule, the nominate gate and the winner names cannot\ndrift between them.\n\n**Four lists, served whole.** There are no pagination parameters: the page\nis four fixed lists, and \"Recent winners\" is *capped* rather than paginated\n(`meta.recent_winners_limit`, currently 6 — the same depth the web page has\nalways shown). A list's length IS its total.\n\n* `needs_my_review` — closed committee cycles this caller is asked to\n  decide. Rendered ABOVE everything else, as the web page's amber\n  \"Needs your review\" card.\n* `open_now` — cycles accepting nominations right now. The hero list, and\n  the only one carrying a CTA.\n* `opening_soon` — scheduled cycles whose window has not started.\n* `recent_winners` — decided cycles, each with its winners (and their\n  printable certificates) and the full honor roll.\n\n**Which list a cycle lands in is NOT its stored status.** It is the\n*effective* status, which reconciles the stored column against the live\nwindow: a cycle stored `scheduled` whose `opens_at` has passed is served\nunder `open_now` with `status: \"open\"`, and one stored `open` whose\n`closes_at` has passed is served in neither browse list. Read the `status`\nthe card reports, never re-derive it from the timestamps. Archived cycles\nare never served.\n\n**No role branching.** An employee, a manager and a recognition admin\nreceive exactly the *same* `open_now`, `opening_soon` and\n`recent_winners`. Verified against the live page in the dev tenant: all\nfour personas saw the identical three open, two upcoming and two decided\ncycles. Only two things differ per caller, and both delegate to the\npredicate the submit path enforces:\n\n* `needs_my_review` — the only per-viewer list. A caller is asked only if\n  the cycle is a *closed committee* cycle, they are on its committee, and\n  they are **not themselves nominated in it** — being up for an award\n  recuses you from deciding it. An admin who is not on the committee is\n  deliberately NOT nagged, mirroring the web list; `can_review` reports\n  the wider gate (which does admit an admin) separately, so a client can\n  still offer them the link.\n* `can_nominate` / `nomination_block_reason` — the window must still be\n  live AND the caller must be allowed to give peer recognition at all.\n  **Render `nomination_block_reason`** instead of a bare disabled button;\n  a blocked card always carries one, and an offered card never does.\n\n**`nominated_by_me` is state, not a gate.** A cycle *pools* nominations, so\na caller who has already nominated may nominate again. The flag exists so\na client can say \"you've nominated\" rather than re-offering a fresh CTA\nwith no memory.\n\n**A winner may have no certificate.** The decide step logs and continues\nwhen an Award can't be minted, so `winners[].certificate` is nullable.\nHandle the null rather than assuming a certificate is always there. When\npresent it carries the printed certificate's fields in its own order —\ntitle, `awarded_by`, the italic `citation` quote, the program / points /\ncompany-value chips, then the footer's `unit` (the issuing organization)\nand date — plus `certificate_url` for the printable page.\n\n**`meta.empty_state` is present only when `open_now` is empty.** It carries\nthe web page's own copy, including the conditional \"check the Opening soon\nlist below\" clause, which appears only when `opening_soon` is non-empty.\n\n**Gated on Model B.** Award cycles are OFF by default. The web page\nredirects a tenant that hasn't opted in; this endpoint answers 403\n`feature_disabled` rather than serving three empty lists that would read as\n\"your company runs no awards\". Access is additionally gated the same way as\nevery other endpoint in this namespace — the Recognitions app must be\nenabled for the tenant AND the caller must be inside the app's audience,\notherwise 403 `access_denied`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The Award Cycles screen",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "award_cycles"
                  ],
                  "properties": {
                    "award_cycles": {
                      "type": "object",
                      "required": [
                        "viewer",
                        "features",
                        "needs_my_review",
                        "open_now",
                        "opening_soon",
                        "recent_winners",
                        "meta"
                      ],
                      "properties": {
                        "viewer": {
                          "type": "object",
                          "required": [
                            "id",
                            "name",
                            "can_give_recognition",
                            "is_recognition_admin",
                            "has_cycles_to_review"
                          ],
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 412
                            },
                            "name": {
                              "type": "string",
                              "example": "Maya Chen"
                            },
                            "image": {
                              "type": "string",
                              "nullable": true,
                              "description": "Absolute avatar URL, or null when it can't be resolved."
                            },
                            "can_give_recognition": {
                              "type": "boolean",
                              "description": "Whether this tenant lets THIS caller give peer recognition. When false, every `can_nominate` below is false too — the same guard the submit path enforces.",
                              "example": true
                            },
                            "is_recognition_admin": {
                              "type": "boolean",
                              "description": "Business admin-or-above, or a Recognitions app admin.",
                              "example": false
                            },
                            "has_cycles_to_review": {
                              "type": "boolean",
                              "description": "True when `needs_my_review` is non-empty — the web page's amber review card in one boolean.",
                              "example": false
                            }
                          }
                        },
                        "features": {
                          "type": "object",
                          "description": "The tenant toggles, so a client hides the same surfaces the web nav hides.",
                          "required": [
                            "award_cycles_enabled",
                            "award_requests_enabled"
                          ],
                          "properties": {
                            "award_cycles_enabled": {
                              "type": "boolean",
                              "description": "Model B. Always true in a 200 — the endpoint 403s otherwise. Present so one payload shape describes both nomination models.",
                              "example": true
                            },
                            "award_requests_enabled": {
                              "type": "boolean",
                              "description": "Model A (ad-hoc award requests). Defaults on.",
                              "example": true
                            }
                          }
                        },
                        "needs_my_review": {
                          "type": "array",
                          "description": "Closed committee cycles THIS caller is asked to decide. The only per-viewer list. Empty for anyone who is not on a committee, and for a committee member who is themselves nominated in the cycle.",
                          "items": {
                            "$ref": "#/components/schemas/RecognitionAwardCycleReviewCard"
                          }
                        },
                        "open_now": {
                          "type": "array",
                          "description": "Cycles accepting nominations right now, newest deadline first. Empty → render `meta.empty_state`, not an error.",
                          "items": {
                            "$ref": "#/components/schemas/RecognitionAwardCycleOpenCard"
                          }
                        },
                        "opening_soon": {
                          "type": "array",
                          "description": "Scheduled cycles whose window has not started. Never offers a CTA — the submit path would refuse.",
                          "items": {
                            "$ref": "#/components/schemas/RecognitionAwardCycleUpcomingCard"
                          }
                        },
                        "recent_winners": {
                          "type": "array",
                          "description": "Decided cycles, most recently closed first, capped at `meta.recent_winners_limit`.",
                          "items": {
                            "$ref": "#/components/schemas/RecognitionAwardCycleDecidedCard"
                          }
                        },
                        "meta": {
                          "type": "object",
                          "required": [
                            "counts",
                            "recent_winners_limit",
                            "closing_soon_days",
                            "capabilities"
                          ],
                          "properties": {
                            "counts": {
                              "type": "object",
                              "description": "The length of each list. Always agrees with the arrays — every list is served whole.",
                              "properties": {
                                "open_now": {
                                  "type": "integer",
                                  "example": 3
                                },
                                "opening_soon": {
                                  "type": "integer",
                                  "example": 2
                                },
                                "recent_winners": {
                                  "type": "integer",
                                  "example": 2
                                },
                                "needs_my_review": {
                                  "type": "integer",
                                  "example": 1
                                }
                              }
                            },
                            "recent_winners_limit": {
                              "type": "integer",
                              "description": "The cap on `recent_winners`. Say \"showing the last N\" rather than implying the tenant has only ever run N cycles.",
                              "example": 6
                            },
                            "closing_soon_days": {
                              "type": "integer",
                              "description": "The window `closing_soon` is computed against. Advisory — it gates nothing.",
                              "example": 3
                            },
                            "empty_state": {
                              "type": "object",
                              "nullable": true,
                              "description": "Present ONLY when `open_now` is empty. The web page's own copy, so the native screen says what the web screen says.",
                              "properties": {
                                "key": {
                                  "type": "string",
                                  "example": "open_now"
                                },
                                "title": {
                                  "type": "string",
                                  "example": "No awards open right now"
                                },
                                "message": {
                                  "type": "string",
                                  "description": "The \"check the Opening soon list below\" clause appears only when `opening_soon` is non-empty.",
                                  "example": "When an award cycle opens for nominations, it will show up here — check the \"Opening soon\" list below."
                                }
                              }
                            },
                            "capabilities": {
                              "type": "object",
                              "properties": {
                                "can_give_recognition": {
                                  "type": "boolean",
                                  "example": true
                                },
                                "award_cycles_enabled": {
                                  "type": "boolean",
                                  "example": true
                                },
                                "award_requests_enabled": {
                                  "type": "boolean",
                                  "example": true
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "The caller's unread notification count, for the app badge.",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Either the Recognitions app is not enabled for the tenant / this user is outside the app's audience (error code `access_denied`), or the tenant has not turned award cycles on (error code `feature_disabled`), which is the API's answer to the web page's redirect."
          }
        }
      }
    },
    "/recognitions/award_cycles/{id}/results": {
      "get": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Award Results — one decided cycle's winners, certificates and honor roll",
        "description": "The native-client mirror of the web **Award Results** page\n(`/recognition/award-cycles/{id}/results`) — the screen a \"Recent winners\"\nrow opens: the trophy hero with the cycle name and announce date, one card\nper winner with their printable certificate, and the honor roll thanking\neveryone who was nominated.\n\n**The body IS a `recent_winners` card.** `award_cycle_results` is the same\n`RecognitionAwardCycleDecidedCard` that\n`GET /recognitions/award_cycles` serves under `recent_winners`, plus a\n`meta` block carrying the results page's copy. Both are produced by one\nserializer, so a client that already holds the row can render this screen\nfrom the same model and refresh it from here — and the two can never name\na different winner, a different certificate or a different honor roll.\n\n**Why the endpoint exists at all.** The list *caps* \"Recent winners\" at\n`meta.recent_winners_limit` (6) and never pages past it. Results for the\nseventh cycle back — reached from a notification, a shared certificate or\na search hit — appear in no list response, and the web page has always been\nable to show them. This is that lookup.\n\n**A winner may have no certificate.** The decide step logs and continues\nwhen an Award can't be minted, so `winners[].certificate` is nullable.\nHandle the null rather than assuming a certificate is always there.\n\n**`winners` may be empty while `honor_roll` is not.** Nobody clearing a\nSpotlight cycle's threshold is a real outcome, not an error — people were\nnominated, none met the bar. `meta.empty_state` carries the web page's own\nline for it, and the honor roll still stands.\n\n**Nothing viewer-wide is served here.** There is no `viewer` block and no\n`needs_my_review`: this endpoint's query object is narrowed to ONE cycle,\nso any capability computed from it would report \"nothing to review\" while\nother cycles wait on the caller. Read those from\n`GET /recognitions/award_cycles`.\n\n**No role branching.** Results are a company-wide celebration — the web\naction has no role check — so an employee, a manager and a recognition\nadmin receive byte-identical payloads.\n\n**Gated on Model B**, exactly like the list: award cycles are OFF by\ndefault, and a tenant that hasn't opted in gets 403 `feature_disabled`\nrather than a deep link that works behind a hidden tab. Access is\nadditionally gated the same way as every other endpoint in this namespace\n— the Recognitions app must be enabled for the tenant AND the caller must\nbe inside the app's audience, otherwise 403 `access_denied`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The award cycle's id — the `id` on any `recent_winners` card. Scoped to the caller's tenant; another tenant's cycle 404s, indistinct from a missing one.",
            "example": 11
          }
        ],
        "responses": {
          "200": {
            "description": "The Award Results screen for one decided cycle",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "award_cycle_results"
                  ],
                  "properties": {
                    "award_cycle_results": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/RecognitionAwardCycleDecidedCard"
                        },
                        {
                          "type": "object",
                          "required": [
                            "meta"
                          ],
                          "properties": {
                            "meta": {
                              "$ref": "#/components/schemas/RecognitionAwardCycleResultsMeta"
                            }
                          }
                        }
                      ]
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "The caller's unread notification count, for the app badge.",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Either the Recognitions app is not enabled for the tenant / this user is outside the app's audience (error code `access_denied`), or the tenant has not turned award cycles on (error code `feature_disabled`)."
          },
          "404": {
            "description": "No such cycle in this tenant. Another tenant's cycle answers the same way as a missing one — the endpoint is enumeration-safe."
          },
          "409": {
            "description": "The cycle exists but has **not been decided yet** (error code\n`results_unavailable`) — the API's answer to the web page's \"Results\naren't available yet.\" redirect. Deliberately not a 404: the cycle is\nreal and will have results, so say \"not announced yet\", never \"gone\".\n\nCovers every non-decided state, including **archived** — `status` is\none column, so archiving replaces \"decided\" and the web page redirects\nan archived cycle too.\n\n`error.details` carries `cycle_id` and the cycle's effective `status`\n(`scheduled` / `open` / `closed` / `archived`), so a client can say\n*why* without a second request.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "results_unavailable"
                        },
                        "message": {
                          "type": "string",
                          "example": "Results aren't available yet."
                        },
                        "details": {
                          "type": "object",
                          "properties": {
                            "cycle_id": {
                              "type": "integer",
                              "example": 10
                            },
                            "status": {
                              "type": "string",
                              "enum": [
                                "scheduled",
                                "open",
                                "closed",
                                "archived"
                              ],
                              "example": "closed"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/recognitions/award_cycles/{id}/review": {
      "get": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Committee review — the anonymized nominee pool for one closed cycle",
        "description": "The native-client mirror of the web **Committee review** page\n(`/recognition/award-cycles/{id}/review`) — the screen a \"Needs your\nreview\" card opens: the anonymized pool of everyone nominated in a closed\ncommittee cycle, what colleagues wrote about them, who else is on the\ncommittee, and this member's own saved ballot.\n\nBoth surfaces run the same query object (`Recognition::CycleReviewQuery`),\nso the gate, the pool, its order and the justifications shown cannot drift\nbetween them. The **write** — `POST /recognitions/award_cycles/{id}/picks`\n— runs the same gate, so a POST can never be accepted on a cycle whose GET\nwould have refused.\n\n**The body opens with a `needs_my_review` card.** Every field of\n`RecognitionAwardCycleReviewCard` is here, produced by the same serializer\n`GET /recognitions/award_cycles` uses, so a client that arrived from that\nlist renders this screen from one model and the two can never disagree on\nthe deadline, the prize or the pick count.\n\n**Anonymity is the product, not a formatting choice.** `justifications`\ncarry text and an opaque id — no nominator, and nothing that can be joined\nback to one. The running committee signal (how many *other* members picked\neach nominee) is absent for the same reason the web page omits it: a member\nwho can see the tally is being anchored rather than asked.\n\n**The pool is served whole and is NOT paginated** — deliberately, and for\nthe same reason the web page isn't: a submit replaces the member's entire\nballot, so a member deciding from a second page would wipe the first page's\npicks. `pool_count` is a total, not a page size.\n\n**Ordering is deterministic**: tally descending, then name, then id. Two\nidentical requests deal the same rows, quoting the same colleagues in the\nsame order.\n\n**`justifications` is capped** at `meta.justification_preview_limit` (4).\n`additional_justification_count` is the remainder — render \"…and N more\"\nrather than implying you have them all.\n\n**The caller's own row can never be picked.** It is flagged `is_me` with\n`can_pick: false`, and the submit path strips the id even if it is sent.\nThis holds for an admin who was let past the recusal gate too.\n\n**An empty pool is a 200, not an error** — a committee cycle nobody\nnominated into is a real state, and `meta.empty_state` carries the web\npage's own line for it.\n\n**Gated on Model B**, exactly like the list: award cycles are OFF by\ndefault, and a tenant that hasn't opted in gets 403 `feature_disabled`.\nAccess is additionally gated the same way as every other endpoint in this\nnamespace — the Recognitions app must be enabled for the tenant AND the\ncaller must be inside the app's audience, otherwise 403 `access_denied`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The award cycle's id — the `id` on any `needs_my_review` card. Scoped to the caller's tenant; another tenant's cycle 404s, indistinct from a missing one.",
            "example": 10
          }
        ],
        "responses": {
          "200": {
            "description": "The committee review screen for one closed committee cycle",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "award_cycle_review"
                  ],
                  "properties": {
                    "award_cycle_review": {
                      "$ref": "#/components/schemas/RecognitionAwardCycleReview"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "The caller's unread notification count, for the app badge.",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "One of four refusals, distinguished by `error.code`:\n\n* `access_denied` — the Recognitions app is not enabled for the\n  tenant, or this user is outside the app's audience.\n* `feature_disabled` — the tenant has not turned award cycles on.\n* `not_committee_member` — the caller is neither on this cycle's\n  committee nor a business admin.\n* `recused` — the caller is a committee member who is themselves\n  nominated in this cycle. Being up for an award recuses you from\n  deciding it. (A business admin is admitted anyway, matching the web,\n  but still cannot pick themselves.)\n\nThe last two carry `error.details` with `cycle_id` and the cycle's\neffective `status`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionCycleReviewRefusal"
                }
              }
            }
          },
          "404": {
            "description": "No such cycle in this tenant. Another tenant's cycle answers the same way as a missing one — the endpoint is enumeration-safe."
          },
          "409": {
            "description": "The cycle exists but there is nothing to review, by `error.code`:\n\n* `not_committee_cycle` — a **Spotlight** cycle. Nobody reviews it;\n  nominees clear a threshold instead.\n* `cycle_not_closed` — nominations are **still open** (the pool would\n  be partial), or the admin has **already decided** the cycle (the\n  review is inert). Both are the web page's \"This cycle isn't open for\n  committee review.\" redirect.\n\nDeliberately not a 404: the cycle is real and the caller may well be\nentitled — this just isn't the moment. `error.details` carries\n`cycle_id` and the effective `status` so a client can say *why*\nwithout a second request.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionCycleReviewRefusal"
                }
              }
            }
          }
        }
      }
    },
    "/recognitions/award_cycles/{id}/nominations/new": {
      "get": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Nominate composer — the form for putting a colleague forward",
        "description": "The native-client mirror of the web **Nominate** page\n(`/recognition/award-cycles/{id}/nominate`), the screen the Award Cycles\nlist's **Nominate** CTA opens.\n\n`cycle` is the **same `open_now` card** `GET /recognitions/award_cycles`\nserves, so a client that arrived from that list renders this screen from\none model and the two can never disagree on the deadline, the prize, the\ncriteria, or whether this viewer may nominate. It carries `can_nominate`\nand — when that is false — `nomination_block_reason`, the sentence to show\ninstead of a dead disabled button.\n\n**It ships no nominee roster.** The `nominee_id` typeahead is\n`GET /recognitions/employee_suggestions`, named in\n`form.nominee_search_url`, so this screen does not carry a second copy of\nthe tenant's directory. It also does not list who else has been nominated:\nthe pool is anonymous until the cycle is decided, and a nominator who\ncould see it would be anchored — the same reasoning the committee review\nscreen records for omitting its running tally.\n\n**The GET is not gated on the window.** A scheduled or closed cycle still\nrenders, with `can_nominate: false` and the reason — matching the web page,\nand giving a client something to draw. Only the POST refuses (409).\n\n**Givers only.** Nominating is a *give*, so both verbs are gated on\n`Recognition::GivingAccess.can_give?` — the tenant's peer-recognition\nswitch, plus managers and admins always. That is the same predicate behind\n`can_nominate` on every cycle card and behind the web page's own\n`enforce_peer_recognition!` bounce, so a client is never offered a form\nwhose POST would be refused.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The award cycle being nominated into. Scoped to the caller's tenant.",
            "example": 12
          }
        ],
        "responses": {
          "200": {
            "description": "The cycle, the form and the page's copy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "award_cycle_nomination_form"
                  ],
                  "properties": {
                    "award_cycle_nomination_form": {
                      "$ref": "#/components/schemas/RecognitionCycleNominationForm"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "The caller's unread notification count, for the app badge.",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`access_denied` (Recognitions app off, or the caller is outside its audience), `feature_disabled` (the tenant hasn't turned award cycles on) or `giving_not_allowed` (this caller may not give recognition, so may not nominate). The giving gate runs BEFORE the cycle is looked up, so a caller without permission cannot use this endpoint to discover which cycle ids exist.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionCycleNominationRefusal"
                }
              }
            }
          },
          "404": {
            "description": "No such cycle in this tenant."
          }
        }
      }
    },
    "/recognitions/award_cycles/{id}/nominations": {
      "post": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Nominate a colleague for an award cycle",
        "description": "The native-client mirror of the web **Submit nomination** button\n(`POST /recognition/award-cycles/{id}/nominate`). Both surfaces run the\nsame service (`Recognition::CycleNominationSubmission`), so the giving\ngate, the window check, the nominee rule, the pooled status, the default\ntitle and the confirmation copy are the same code.\n\n**This is not `POST /recognitions/nominations`.** That endpoint files a\nModel A ad-hoc award *request*, which routes to a reviewer and can mint an\nAward on the spot. A cycle nomination is **pooled**: it is born `pending`\nand stays there until the cycle closes and is decided — by the Spotlight\n`threshold_nominators` count, or by a committee ballot through\n`POST /recognitions/award_cycles/{id}/picks`. There is no reviewer on this\npath and no per-nomination approve/reject, which is why the response\ncarries `pooled: true` and no `requires_approval`.\n\n**The program comes from the cycle.** `recognition_program_id` is not\naccepted: a cycle nomination belongs to the program running the cycle, and\nsending one is ignored rather than honoured.\n\n**Nominate again, yes; the same person twice, no.** A cycle pools\nnominations, so one nominator may put several *different* colleagues\nforward — `meta.allows_multiple_nominations` on the composer says so, and\na client must not hide the CTA after one submit. The model's duplicate\nguard is keyed on (nominator, nominee, cycle), so a second nomination of\nthe *same* person is refused with 422 `validation_failed`. That is exactly\nwhat makes the Spotlight threshold mean \"N **distinct** colleagues\".\n\n**Send an `Idempotency-Key`.** The retry is replayed *before* anything is\nwritten, so a client that times out and retries gets its original 201 back\nrather than tripping the duplicate guard and being told it had already\nnominated someone it does not think it nominated.\n\nThe `nomination` wrapper is optional — the fields may be sent flat, and\nthe web form's `nomination[...]` shape is accepted unchanged.\n\nGated identically to the composer above: same app access, same Model B\ntoggle, same giving permission, same tenant scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The award cycle being nominated into. Scoped to the caller's tenant.",
            "example": 12
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Retry-safety. A repeat with the same key replays the original 201 and writes nothing — checked before the write, not after.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecognitionCycleNominationRequest"
              },
              "examples": {
                "minimal": {
                  "summary": "The two fields the form actually asks for",
                  "value": {
                    "nomination": {
                      "nominee_id": 893,
                      "description": "Rebuilt the onboarding checklist and cut new-hire ramp by a week."
                    }
                  }
                },
                "flat": {
                  "summary": "The same body without the wrapper",
                  "value": {
                    "nominee_id": 893,
                    "description": "Rebuilt the onboarding checklist and cut new-hire ramp by a week."
                  }
                },
                "with_title": {
                  "summary": "Overriding the title (defaults to the cycle's name)",
                  "value": {
                    "nomination": {
                      "nominee_id": 893,
                      "title": "The onboarding rebuild",
                      "description": "Rebuilt the onboarding checklist and cut new-hire ramp by a week."
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The nomination as filed, plus the cycle card with it counted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "award_cycle_nomination"
                  ],
                  "properties": {
                    "award_cycle_nomination": {
                      "$ref": "#/components/schemas/RecognitionCycleNominationResult"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "The caller's unread notification count, for the app badge.",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`access_denied`, `feature_disabled` or `giving_not_allowed` — the same three refusals, with the same wording, as the composer above. A rule that guarded only the form is a rule a direct POST walks past.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionCycleNominationRefusal"
                }
              }
            }
          },
          "404": {
            "description": "No such cycle in this tenant."
          },
          "409": {
            "description": "`cycle_not_open` — the cycle is real and the caller is entitled, but the nomination window isn't live: scheduled, closed, decided, archived, or force-closed by an admin while `closes_at` is still in the future. The state is read through the cycle's EFFECTIVE status, so the stored `status` column is not the answer, and `error.details.status` carries the reconciled value a client should re-render from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionCycleNominationRefusal"
                }
              }
            }
          },
          "422": {
            "description": "One of three, and nothing is written in any of them:\n\n* `invalid_nominee` — no `nominee_id` was sent, or the id is not an\n  active member of this tenant.\n* `validation_failed` — the model refused: a self-nomination, a second\n  nomination of the same person in this cycle, or a reason outside\n  10–1000 characters.\n* `content_rejected` — the tenant's content-moderation keyword screen\n  refused the submitted words. This is not cosmetic here: the\n  `description` becomes the cycle's org-wide \"kind words\", which are\n  emailed to the winners **and** to every non-winner who was\n  nominated, so unscreened text would be mailed to the person it was\n  written about. Only what the caller SUBMITTED is screened — a title\n  defaulted from the cycle's own name never is.\n\n`validation_failed` and `content_rejected` both carry\n`error.details.field_errors`, so a native form can mark the offending\nfield instead of parsing the sentence.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionCycleNominationRefusal"
                }
              }
            }
          }
        }
      }
    },
    "/recognitions/award_cycles/{id}/picks": {
      "post": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Save a committee ballot — one nominee or several",
        "description": "The native-client mirror of the web review page's **Save my\nrecommendations** button (`POST /recognition/award-cycles/{id}/review`).\nBoth surfaces run the same service (`Recognition::CyclePickSubmission`),\nso the gate, the pool filter, the never-vote-for-yourself rule and the\nreplace semantics are the same code.\n\n**Single or multiple, one call.** Send `nominee_ids` (an array, or a\ncomma-separated string) or `nominee_id` (one scalar) — a member picking one\nperson and a member picking six use the same request.\n\n**It REPLACES the ballot; it does not merge.** A submit is the member's\ncomplete set of recommendations for this cycle: their existing picks are\ndeleted and re-created from what was sent, so sending one id makes that id\nthe *only* pick. This is why the review screen is not paginated — send the\nwhole selection, never a delta.\n\n**An explicit empty selection clears the ballot** (`{\"nominee_ids\": []}`),\nwhich is how a member withdraws. **Omitting both keys is refused** with 422\n`no_selection` rather than read as \"clear everything\" — the one place this\nendpoint is stricter than the web form, which always posts its checkbox set.\n\n**Ids that cannot be picked are dropped, not fatal.** Anyone not in this\ncycle's nominee pool, and the caller's own id, are removed and listed in\n`ignored_ids`. The web renders no checkbox for either; a non-empty\n`ignored_ids` means the client's list is stale.\n\n**Repeated ids are de-duplicated** — a nominee is picked once or not at all.\n\n**`changed` reports whether anything actually moved**, so a client can skip\na needless \"saved\" toast on a no-op re-submit.\n\nNothing here touches another committee member's ballot, and the running\ntally is never returned — read the outcome from\n`GET /recognitions/award_cycles/{id}/review`, and the decided winners from\n`GET /recognitions/award_cycles/{id}/results` once an admin has decided.\n\nGated identically to the GET above — same app access, same Model B toggle,\nsame four-rule review gate, same status codes.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The award cycle being decided. Scoped to the caller's tenant.",
            "example": 10
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecognitionCyclePicksRequest"
              },
              "examples": {
                "multiple": {
                  "summary": "Several nominees (the usual case)",
                  "value": {
                    "nominee_ids": [
                      893,
                      265
                    ]
                  }
                },
                "single": {
                  "summary": "One nominee, via the scalar shorthand",
                  "value": {
                    "nominee_id": 893
                  }
                },
                "comma_separated": {
                  "summary": "The web's hidden-field form",
                  "value": {
                    "nominee_ids": "893,265"
                  }
                },
                "clear": {
                  "summary": "Withdraw — clear this member's ballot",
                  "value": {
                    "nominee_ids": []
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The ballot after the write",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "award_cycle_picks"
                  ],
                  "properties": {
                    "award_cycle_picks": {
                      "$ref": "#/components/schemas/RecognitionCyclePicksResult"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "The caller's unread notification count, for the app badge.",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`access_denied`, `feature_disabled`, `not_committee_member` or `recused` — the same four refusals, with the same wording, as the GET above. A rule that guarded only the read would be a rule a direct POST walked past.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionCycleReviewRefusal"
                }
              }
            }
          },
          "404": {
            "description": "No such cycle in this tenant."
          },
          "409": {
            "description": "`not_committee_cycle` or `cycle_not_closed` — as the GET above. Also returned when the cycle changes state *between* a client's read and its write (an admin decided it mid-ballot); nothing is saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionCycleReviewRefusal"
                }
              }
            }
          },
          "422": {
            "description": "Neither `nominee_ids` nor `nominee_id` was sent (error code\n`no_selection`). The saved ballot is left untouched. To clear it,\nsend an explicit empty array.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "no_selection"
                        },
                        "message": {
                          "type": "string",
                          "example": "Send nominee_ids (or nominee_id). Pass an empty nominee_ids to clear your recommendations."
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/recognitions/approvals": {
      "get": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Pending Approvals queue — nominations + posts awaiting review",
        "description": "The native-client mirror of the web **Pending Approvals** page, reached\nfrom Team ▸ Pending Approvals ▸ **View all**. Both surfaces read the same\nquery object (`Recognition::ApprovalsQueue`), so the two cannot drift on\nwhich rows a reviewer sees, in what order, or how the filters behave.\n\n**Two lists**, exactly as the web page stacks them:\n\n* `nominations` — the ad-hoc award requests awaiting this reviewer's\n  decision (cycle nominations are excluded; those are decided through the\n  cycle, never one at a time).\n* `posts` — peer recognition held for this reviewer's **manager\n  approval**, still unpublished until a reviewer decides.\n\nEach list paginates **independently** (`page` for nominations,\n`posts_page` for posts) because a reviewer can be deep in one while the\nother is short; a single shared cursor would strand rows.\n\n**Reviewer-only, and the reviewable rule is emergent.** The endpoint is\ngated on the canonical `User#recognition_reviewer?` (a business admin, a\nRecognitions app admin, or a manager with direct reports) and refuses\neveryone else with **403 `forbidden`** rather than an empty queue that\nwould read as \"nothing to approve\". WITHIN that gate the two personas see\ndifferent sets, decided by data not by a role flag: an **admin** sees the\nwhole tenant's queue; a **manager** sees nominations assigned to them,\nunassigned (available for any approver), or nominating one of their own\nreports, plus the posts routed to them. `viewer.is_recognition_admin`\ntells the client which it is.\n\nThis endpoint is **read-only**. Acting on the queue is the sibling\nendpoints' job: the bulk approve/reject actions and the per-nomination\napprove/reject routes. AI-flagged content is NOT part of this queue — it\nis reviewed in the separate unified Content Moderation Queue.\n\nValues that represent money are reported in **reward points**; storage is\nin dollars and the conversion happens server-side.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Case-insensitive search. Matches the nominee/recipient name and the nomination title / post content across BOTH lists.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "program_id",
            "in": "query",
            "required": false,
            "description": "Narrow the NOMINATIONS list to one recognition program. Posts have no program in this queue and are unaffected. The programs actually present in the queue are returned in `filters.program_options` (derived from the UNFILTERED queue, so switching never requires clearing the filter first).",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "overdue",
            "in": "query",
            "required": false,
            "description": "When truthy, keep only NOMINATIONS submitted before the tenant's reminder window (the `nomination_reminder_days` setting, clamped to 1..60, default 7 days) — the \"waiting longest\" filter.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based page number for the NOMINATIONS list. Values below 1 are treated as 1.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "posts_page",
            "in": "query",
            "required": false,
            "description": "1-based page number for the POSTS list (independent of `page`).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Items per page, applied to both lists. Clamped to 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The reviewer's pending-approvals queue.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "approvals": {
                      "type": "object",
                      "properties": {
                        "viewer": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "image": {
                              "type": "string",
                              "nullable": true
                            },
                            "is_recognition_admin": {
                              "type": "boolean",
                              "description": "True when this reviewer sees the WHOLE tenant's queue; false for a manager scoped to their own team."
                            },
                            "can_give_recognition": {
                              "type": "boolean"
                            }
                          }
                        },
                        "capabilities": {
                          "type": "object",
                          "description": "Viewer-level toggles the client uses to render the queue's global actions.",
                          "properties": {
                            "quick_award_enabled": {
                              "type": "boolean"
                            },
                            "award_requests_enabled": {
                              "type": "boolean"
                            },
                            "award_cycles_enabled": {
                              "type": "boolean"
                            }
                          }
                        },
                        "filters": {
                          "type": "object",
                          "properties": {
                            "applied": {
                              "type": "object",
                              "description": "The filter values the server actually applied (echoed back for the chips).",
                              "properties": {
                                "q": {
                                  "type": "string",
                                  "nullable": true
                                },
                                "program_id": {
                                  "type": "integer",
                                  "nullable": true
                                },
                                "overdue": {
                                  "type": "boolean"
                                }
                              }
                            },
                            "program_options": {
                              "type": "array",
                              "description": "The programs present in the UNFILTERED queue — the \"Program\" dropdown's options.",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "integer"
                                  },
                                  "name": {
                                    "type": "string"
                                  }
                                }
                              }
                            }
                          }
                        },
                        "nominations": {
                          "type": "object",
                          "properties": {
                            "items": {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/RecognitionApprovalNomination"
                              }
                            },
                            "meta": {
                              "$ref": "#/components/schemas/RecognitionApprovalsPageMeta"
                            }
                          }
                        },
                        "posts": {
                          "type": "object",
                          "properties": {
                            "items": {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/RecognitionApprovalPost"
                              }
                            },
                            "meta": {
                              "$ref": "#/components/schemas/RecognitionApprovalsPageMeta"
                            }
                          }
                        },
                        "total_pending": {
                          "type": "integer",
                          "description": "The whole queue this reviewer must clear — nominations + posts across every page. The badge figure, matching the Team endpoint's `pending_approvals.total`.",
                          "example": 7
                        }
                      }
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "The caller's unread notification count, for the app badge.",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Recognitions app is not accessible to the caller (error code `access_denied`), or the caller is not a reviewer (error code `forbidden`) — the API's answer to the web page's redirect.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/recognitions/nominations": {
      "post": {
        "tags": [
          "Recognitions"
        ],
        "summary": "File a nomination (nominate someone for an award)",
        "description": "Nominate a colleague for an award — the native-client mirror of the web\n\"Nominate Someone\" form (RecognitionController#create_nomination), the\ncomposer behind the feed's **+ ▸ Nominate**. Both surfaces run ONE service\n(`Recognition::NominationSubmission`), so the eligibility gates, the\napproval routing, the reviewer assignment and the confirmation copy are\nliterally the same code.\n\n**Who may call it.** This is the NOMINATOR half of the nominations\ncontroller — unlike `/approve` and `/reject`, it is NOT reviewer-gated.\nThe caller needs three things, each answered up front by\n`GET /recognitions/config`:\n\n* the Recognitions app is accessible to them\n* award requests (Model A) are on for the tenant —\n  `config.features.award_requests_enabled`\n* they may give recognition at all — `config.permissions.can_give`\n  (a tenant that switched peer recognition off leaves this to managers\n  and Recognition admins)\n\n**Build the form from `GET /recognitions/config`.** Its `programs` block\nis the program picker, with `can_nominate` and `nomination_block_reason`\nper program (hide the ones the caller can't use rather than disabling\nthem) and each program's `categories`, each carrying `min_points` /\n`max_points`. `limits` states every bound this endpoint enforces, and\n`economy.points_per_dollar` is the conversion rate. The nominee picker is\n`GET /recognitions/employee_suggestions`.\n\n**Reward points vs stored dollars.** `requested_value` is supplied in\nreward POINTS (what the nominator types), exactly like the web form. The\nrecord stores dollars (1 pt = 1¢ at the default rate) and the server\nconverts for you — do NOT pre-divide.\n\n**Approval routing.** With `require_manager_approval` off (the default)\nthe nomination is filed already `approved` — nobody has to decide it, and\nit never appears in a reviewer's queue. With it on, the nomination is\nfiled `under_review` and routed to the nominee's own manager (falling back\nto the tenant's first active admin), unless the tenant's autonomous\napproval ceiling clears it. Read `requires_approval` and\n`autonomously_approved` on the response rather than inferring from\n`status`.\n\n**Retries.** Send an `Idempotency-Key` header: a retry after a timeout\nreplays the original `201` instead of filing a second nomination. Without\none, the model's duplicate guard is the backstop — a second live\nnomination of the same person for the same award comes back `422`.\n\n**Body shape.** Both the web-shaped `{ \"nomination\": { … } }` and a flat\n`{ … }` body are accepted. Use `multipart/form-data` when attaching\nsupporting evidence.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "nomination": {
                    "$ref": "#/components/schemas/RecognitionNominationInput"
                  }
                },
                "required": [
                  "nomination"
                ]
              }
            },
            "multipart/form-data": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/RecognitionNominationInput"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "supporting_files": {
                        "type": "array",
                        "description": "Optional evidence the approver sees beside the justification — a metrics export, a customer email, a before/after screenshot. Screened server-side by content type (sniffed from the bytes, not trusted from the client), size and count; the exact rules are reported by `config.limits.nomination_supporting_file_*`.",
                        "items": {
                          "type": "string",
                          "format": "binary"
                        }
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The nomination was filed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "nomination",
                    "message",
                    "requires_approval"
                  ],
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "A human-readable outcome, mirroring the web flash verbatim.",
                      "example": "Nomination for Ada Lovelace has been submitted successfully!"
                    },
                    "requires_approval": {
                      "type": "boolean",
                      "description": "True when a reviewer still has to decide (`status` is `under_review`). False means the award is already granted.",
                      "example": false
                    },
                    "autonomously_approved": {
                      "type": "boolean",
                      "description": "True when the tenant DOES require manager approval but this request came in under the autonomous approval ceiling and was cleared without a reviewer (an Automation Hub audit row is written). Lets a client explain why nobody was asked.",
                      "example": false
                    },
                    "nomination": {
                      "$ref": "#/components/schemas/RecognitionNominationDecision"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "The caller's unread notification count, for the app badge.",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "One of five refusals, distinguished by `error.code`:\n\n* `access_denied` — the Recognitions app is not accessible to the caller\n* `feature_disabled` — award requests (Model A) are off for the tenant\n* `giving_not_allowed` — the tenant limits giving to managers and\n  Recognition admins, and the caller is neither\n* `program_unavailable` — the program is not open for nominations right\n  now (inactive, outside its window, a lifecycle/milestone program, or\n  not in this business). The picker never lists these.\n* `not_eligible` — the program IS open, just not to this nominator (a\n  manager-only program, or achievement criteria they don't meet).\n  `config.programs[].nomination_block_reason` says so in their words.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "The nominee is not a member of the caller's business (error code `nominee_not_found`) — indistinct from a user id that doesn't exist, so this endpoint can't be used to enumerate the directory.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The nomination failed validation (error code `validation_failed`) — `error.details.fields` carries the per-field messages so a client can mark the offending inputs. Covers self-nomination, a duplicate live nomination of the same person for the same award, the title and description bounds, a requested value outside the category's range or over the program's per-award cap, and rejected supporting files. A `nomination_failed` code here means the submit raised unexpectedly.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "validation_failed"
                        },
                        "message": {
                          "type": "string",
                          "example": "Description is too short (minimum is 10 characters)"
                        },
                        "details": {
                          "type": "object",
                          "properties": {
                            "fields": {
                              "type": "object",
                              "description": "Per-field validation messages, keyed by attribute.",
                              "additionalProperties": {
                                "type": "array",
                                "items": {
                                  "type": "string"
                                }
                              },
                              "example": {
                                "description": [
                                  "is too short (minimum is 10 characters)"
                                ]
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The nominate rate limit (error code `rate_limited`) — 10 per hour per person per tenant, the SAME counter the web form is behind, so a client can't out-run it by switching surface. `Retry-After` and `error.details.retry_after_seconds` carry the window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/recognitions/nominations/bulk_approve": {
      "post": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Bulk-approve recognition requests (nominations)",
        "description": "Approve MANY award-request nominations in one call — the native-client\nmirror of the web pending-approvals \"Approve selected\" action\n(RecognitionController#bulk_approve_nominations). Both surfaces run the\nSAME service (Recognition::BulkNominationReview), so they can't drift on\nwho may bulk-approve what, on the partial-success behaviour, or on how a\nmixed result is reported.\n\n**Partial success, not all-or-nothing.** Each nomination is approved\nindependently through Nomination#record_approval! (so a multi-level\nprogram advances a request to the next approver rather than minting the\naward early). One failing row does not roll back the others. The 200 body\nbreaks the batch down:\n\n* `processed` — the nominations that were approved, each with its\n  resulting `status` and (for a multi-level chain) the step it advanced to.\n* `skipped_ids` — ids that were NOT acted on because they are not\n  reviewable by the caller, already decided, or belong to another tenant.\n  Never silently approved.\n* `errors` — per-row failures, each naming the nominee and the reason.\n\n**Authorization** — reviewer-only, exactly like the web page: a business\nadmin, a Recognitions app admin, or a manager with direct reports. The\nreviewable rule then narrows the batch to the nominations this caller may\nactually decide; anything outside it comes back in `skipped_ids`.\n\nUp to 200 ids per call.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "nomination_ids"
                ],
                "properties": {
                  "nomination_ids": {
                    "description": "The nominations to approve. An array of ids (preferred) or a comma-separated string. Blanks/zeros are dropped and duplicates collapsed. 1..200 ids.",
                    "oneOf": [
                      {
                        "type": "array",
                        "items": {
                          "type": "integer"
                        }
                      },
                      {
                        "type": "string"
                      }
                    ],
                    "example": [
                      8123,
                      8124,
                      8130
                    ]
                  },
                  "notes": {
                    "type": "string",
                    "nullable": true,
                    "description": "Optional approval note recorded on each resulting award (`reviewer_notes`).",
                    "example": "Q3 spot-award batch — all verified with the managers."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The batch was accepted and applied. Read `processed_count`, `skipped_count` and `error_count` — a 200 can still carry skipped or errored rows.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionBulkReviewResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Recognitions app is not accessible to the caller (error code `access_denied`), or the caller is not a reviewer (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "The request itself was rejected before any nomination was touched: no ids selected (error code `no_ids`) or more than 200 ids (error code `too_many`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/recognitions/nominations/bulk_reject": {
      "post": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Bulk-reject recognition requests (nominations)",
        "description": "Reject MANY award-request nominations in one call — the native-client\nmirror of the web pending-approvals \"Reject selected\" action\n(RecognitionController#bulk_reject_nominations). Both surfaces run the\nSAME service (Recognition::BulkNominationReview).\n\nA **reason is required** — exactly as the web modal requires it — and is\nrecorded on every rejected nomination (`reviewer_notes`) and shown to each\nnominator. A blank/whitespace reason is refused with 422\n`reason_required` and nothing is touched.\n\n**Partial success**, reported the same way as bulk approve: `processed`\n(the rejected nominations), `skipped_ids` (not reviewable / already\ndecided / another tenant's), and `errors` (per-row failures).\n\n**Authorization** — reviewer-only, exactly like the web page: a business\nadmin, a Recognitions app admin, or a manager with direct reports. The\nreviewable rule narrows the batch; anything outside it comes back in\n`skipped_ids`.\n\nUp to 200 ids per call.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "nomination_ids",
                  "reason"
                ],
                "properties": {
                  "nomination_ids": {
                    "description": "The nominations to reject. An array of ids (preferred) or a comma-separated string. Blanks/zeros are dropped and duplicates collapsed. 1..200 ids.",
                    "oneOf": [
                      {
                        "type": "array",
                        "items": {
                          "type": "integer"
                        }
                      },
                      {
                        "type": "string"
                      }
                    ],
                    "example": [
                      8140,
                      8141
                    ]
                  },
                  "reason": {
                    "type": "string",
                    "description": "The reason for rejecting, recorded on every nomination (`reviewer_notes`) and shown to each nominator. Required and non-blank.",
                    "example": "These duplicate awards already granted for the same work."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The batch was accepted and applied. Read the counts — a 200 can still carry skipped or errored rows.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionBulkReviewResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Recognitions app is not accessible to the caller (error code `access_denied`), or the caller is not a reviewer (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "The request itself was rejected before any nomination was touched: the reason is missing/blank (error code `reason_required`), no ids selected (error code `no_ids`), or more than 200 ids (error code `too_many`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/recognitions/nominations/{id}/approve": {
      "post": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Approve a recognition request (nomination)",
        "description": "Approve an award-request nomination — the native-client mirror of the web\napproval action (RecognitionController#approve_nomination). Runs the SAME\ncollaborators the web page runs, so the two surfaces cannot drift: the\nper-record ReviewableNominations authorization rule, the points→dollars\nconversion, and Nomination#record_approval!.\n\nOn the **final** approval level this mints the Award (moving the\nrecognition from \"pending approval\" to a real, points-bearing award) and\nnotifies the recipient. Under a **multi-level** program the request is\ninstead advanced to the next approver up the reporting chain, and\n`approval_complete` comes back `false` with `message` naming where it\nrouted.\n\n**Reward points vs stored dollars:** `approved_value` is supplied in\nreward POINTS (what the reviewer sees), exactly like the web approve\nmodal. The award stores dollars (1 pt = 1¢ at the default rate), and the\nserver converts for you — do NOT pre-divide. Omit `approved_value` to\napprove at the originally requested value.\n\n**Authorization** — reviewer-only, exactly like the web page: a business\nadmin, a Recognitions app admin, or the nominee's manager (anyone with\ndirect reports whose report is the nominee), AND only for a nomination the\ncaller may actually review. A caller who clears the coarse gate but may\nnot review this specific request gets 403.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The nomination (recognition request) id, scoped to the caller's business.",
            "schema": {
              "type": "integer"
            },
            "example": 8123
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "approved_value": {
                    "type": "integer",
                    "nullable": true,
                    "description": "The award value in reward POINTS. Overrides the requested value; converted server-side to stored dollars. Omit or leave blank to approve at the requested value. Must be zero or greater.",
                    "example": 500
                  },
                  "approval_notes": {
                    "type": "string",
                    "nullable": true,
                    "description": "Optional reviewer notes recorded on the nomination (surfaced as `reviewer_notes`).",
                    "example": "Clear, measurable impact on the renewal — well earned."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The request was approved (award minted) or advanced to the next approval level. Read `nomination.approval_complete` to tell which.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "nomination",
                    "message"
                  ],
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "A human-readable outcome, mirroring the web flash.",
                      "example": "Recognition request for Ada Lovelace has been approved."
                    },
                    "nomination": {
                      "$ref": "#/components/schemas/RecognitionNominationDecision"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "The caller's unread notification count, for the app badge.",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Either the Recognitions app is not accessible to the caller (error code `access_denied`), the caller is not a reviewer at all, or the caller may not review THIS nomination (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such nomination in the caller's business (error code `not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "The nomination is already processed (error code `already_processed`), the points value is negative (error code `invalid_points`), or the approval could not be completed (error code `approval_failed`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/recognitions/nominations/{id}/reject": {
      "post": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Reject a recognition request (nomination)",
        "description": "Reject an award-request nomination — the native-client mirror of the web\nreject action (RecognitionController#reject_nomination). Runs the SAME\ncollaborators the web page runs, so the two surfaces cannot drift: the\nper-record ReviewableNominations authorization rule and Nomination#reject!,\nwhich stamps the status, records the reviewer, stores the reason, and\nnotifies the nominator.\n\nA **reason is required** — exactly as the web modal requires it. Send it as\n`rejection_reason` (or its alias `reason`); a blank/whitespace value is\nrefused with 422 `rejection_reason_required` and the nomination is left\nuntouched.\n\nReject is terminal: the returned `nomination.status` is `rejected` and\n`approval_complete` is `true`. The reason is echoed back in\n`reviewer_notes`.\n\n**Authorization** — reviewer-only, exactly like the web page: a business\nadmin, a Recognitions app admin, or the nominee's manager (anyone with\ndirect reports whose report is the nominee), AND only for a nomination the\ncaller may actually review. A caller who clears the coarse gate but may not\nreview this specific request gets 403.\n\n**Idempotent** — send an `Idempotency-Key` header and a retry after a\ntimeout replays the stored response instead of re-running the reject (which\nwould otherwise return 422 `already_processed` for a reject that had in\nfact succeeded).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The nomination (recognition request) id, scoped to the caller's business.",
            "schema": {
              "type": "integer"
            },
            "example": 8123
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "An opaque key that makes a retried reject replay the stored response instead of re-running.",
            "schema": {
              "type": "string"
            },
            "example": "reject-8123-6f1c2a"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "rejection_reason"
                ],
                "properties": {
                  "rejection_reason": {
                    "type": "string",
                    "description": "The reason for rejecting, recorded on the nomination (`reviewer_notes`) and shown to the nominator. Required and non-blank.",
                    "example": "Duplicate of last month's award for the same work."
                  },
                  "reason": {
                    "type": "string",
                    "description": "Alias for `rejection_reason` (for native clients). Whichever is present and non-blank wins.",
                    "example": "Duplicate of last month's award for the same work."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The request was rejected. `nomination.status` is `rejected` and `reviewer_notes` carries the reason.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "nomination",
                    "message"
                  ],
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "A human-readable outcome, mirroring the web flash.",
                      "example": "Recognition request for Ada Lovelace has been rejected."
                    },
                    "nomination": {
                      "$ref": "#/components/schemas/RecognitionNominationDecision"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "The caller's unread notification count, for the app badge.",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Either the Recognitions app is not accessible to the caller (error code `access_denied`), the caller is not a reviewer at all, or the caller may not review THIS nomination (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such nomination in the caller's business (error code `not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "The rejection reason is missing/blank (error code `rejection_reason_required`), the nomination is already processed (error code `already_processed`), or the reject could not be completed (error code `rejection_failed`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/recognitions/posts": {
      "post": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Give recognition",
        "description": "**Give peer recognition.** The native mirror of the web give composer\n(`POST /recognition/posts` → `RecognitionController#create_recognition_post`,\nreached from Feed ▸ **+** ▸ *Give recognition*) and of the mobile give form.\n\nBoth surfaces run `::Recognition::PeerPostCreator`, so the entire gate chain\nis the same code — peer-giving access, the anti-gaming governance guard\n(monthly cap + duplicate-recipient cooldown), keyword and AI content\nmoderation, the anonymity policy, peer-points gating, manager-approval and\nspend-threshold routing, and the atomic group fan-out.\n\n**Who may call it.** Two gates, in order:\n  1. the Recognitions app is accessible to the caller (403 `access_denied`)\n  2. this caller may give peer recognition (403 `giving_not_allowed`) — the\n     same `::Recognition::GivingAccess` predicate behind the web guard and\n     behind `permissions.can_give` on `GET /recognitions/config`. A tenant\n     that switched peer recognition off leaves giving to managers and\n     Recognition admins.\n\n**Render the composer from `GET /recognitions/config`.** Every option\ncatalog and every limit this endpoint validates against is served there —\n`values`, `tags`, `cards`, `economy.point_tiers`, `visibility_options`,\n`limits.max_recipients`, `limits.message_min` / `message_max`. The\nrecipient typeahead is `GET /recognitions/employee_suggestions`; a client\nshould not build its own roster, because only people that query offers are\nacceptable recipients here (active members, service / AI-agent principals\nexcluded).\n\n**A 201 does NOT mean the recognition is live.** Read `status`:\n\n| `status` | what happened |\n|---|---|\n| `active` | published — it is in the feed now |\n| `posting` | held for the automated content screen. **The normal path when AI moderation is on**; it publishes moments later |\n| `pending_review` | keyword/policy hold — a human moderator decides |\n| `pending_approval` | routed to the recipient's manager (manager approval, or a points give over the spend threshold) |\n\n`message` is the web flash for that status, word for word, so a client can\nshow it as-is instead of composing its own (and getting `posting` wrong by\nclaiming the recognition was sent).\n\n**Group gives.** `recipient_ids` may name up to `limits.max_recipients`\npeople. One submission writes one row PER recipient, atomically — a failure\non any row rolls the whole give back, so a group give never half-posts. The\nresponse collapses them into ONE card naming everybody (exactly as the feed\ncollapses a group give) and lists every row's id in `recognition_ids`.\n\n**Points require a value.** `points > 0` needs a `company_value_id` — points\nare always tied to a core value. `points` is silently 0 when the tenant has\npeer points switched off, so the give lands as a plain shout-out rather\nthan being refused.\n\n**Cards.** `award_template_id` takes the picker's selection verbatim: a\ntenant card's integer id, or `\"central:<slug>\"` for a gallery card (whose\nart is copied into a tenant asset on submit). Either way, the card's default\nmessage fills a **blank** `content` — it never overwrites what the giver\ntyped. `award_art_url` on the response is the art that was attached.\n\n**Photos.** The desktop composer uploads a file; a native client sends\n`photo_url` instead — a **public HTTPS** URL it already hosts — and the\nserver fetches the image and stores it on the recognition, so this endpoint\nstays JSON and the response card carries the same `photo_url` every read\nendpoint reports. That URL is **our** stored WebP rendition, not the one you\nsent: the bytes now live here, and the source link is never referenced\nagain.\n\nThe fetch is guarded, and every one of these refuses the give with\n`invalid_photo_url` (422) **before anything is written** — no row, no\nnotification:\n\n| rule | why |\n|---|---|\n| public HTTPS only | loopback, private, link-local, CGNAT and cloud-metadata addresses are refused, embedded credentials are refused, and each redirect hop is revalidated |\n| JPG / PNG / WebP / HEIC | decided by **sniffing the bytes**, not by your `Content-Type` header or the URL's extension |\n| 10 MB | the same cap the web composer enforces |\n| reachable within ~10s | the fetch is inline, because a recognition that published without its photo and notified everyone cannot be un-sent |\n\nOn a **group give** the photo is fetched and stored **once** and the blob is\nshared across every row.\n\n**Self-recognition** is dropped, not refused — if the caller is the only\nrecipient named, the give is refused with `no_recipients`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecognitionGiveRequest"
              },
              "examples": {
                "shoutOut": {
                  "summary": "Plain shout-out to one person",
                  "value": {
                    "recipient_id": 812,
                    "content": "You carried the migration all weekend — thank you."
                  }
                },
                "withPoints": {
                  "summary": "Points give (a company value is required)",
                  "value": {
                    "recipient_ids": [
                      812,
                      907
                    ],
                    "content": "The two of you turned a nasty outage into a non-event.",
                    "points": 250,
                    "company_value_id": 4,
                    "recognition_tags": [
                      "teamwork",
                      "above-and-beyond"
                    ],
                    "visibility": "department"
                  }
                },
                "galleryCard": {
                  "summary": "Gallery card, message left blank so the card's copy is used",
                  "value": {
                    "recipient_id": 812,
                    "award_template_id": "central:thank-you-star",
                    "content": ""
                  }
                },
                "anonymous": {
                  "summary": "Anonymous give (only when the tenant allows it)",
                  "value": {
                    "recipient_id": 812,
                    "content": "Quietly fixed the thing nobody else wanted to touch.",
                    "is_anonymous": true
                  }
                },
                "withPhoto": {
                  "summary": "With a photo, named by URL",
                  "value": {
                    "recipient_id": 812,
                    "content": "Here's the install she finished single-handed on Saturday.",
                    "photo_url": "https://cdn.example.com/uploads/install-day.jpg"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The recognition was created. Check `status` before telling the user it is live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionGiveResult"
                },
                "examples": {
                  "published": {
                    "summary": "Published immediately",
                    "value": {
                      "recognition": {
                        "type": "recognition_post",
                        "kind": "recognition",
                        "id": 4471,
                        "message": "You carried the migration all weekend — thank you.",
                        "points": 250,
                        "recipient": {
                          "id": 812,
                          "name": "Priya Nair",
                          "title": "SRE",
                          "image": null
                        },
                        "giver": {
                          "id": 55,
                          "name": "Sofia Ahmed",
                          "title": "Engineering Manager",
                          "image": null
                        },
                        "anonymous": false,
                        "company_value": "Customer First",
                        "occurred_at": "2026-08-17T10:24:01Z",
                        "visibility": "public",
                        "status": "active",
                        "outcome_status": "active",
                        "tags": [
                          "teamwork"
                        ],
                        "photo_url": null,
                        "award_art_url": null,
                        "boost": {
                          "total_points": 0,
                          "boosted_by_me": false,
                          "can_boost": false,
                          "amounts": []
                        }
                      },
                      "recognition_ids": [
                        4471
                      ],
                      "status": "active",
                      "message": "Recognition for Priya Nair has been shared!",
                      "giving_remaining": 750,
                      "unread_notification_count": 3
                    }
                  },
                  "posting": {
                    "summary": "Held for the automated content screen (the normal AI-moderation path)",
                    "value": {
                      "recognition": {
                        "type": "recognition_post",
                        "kind": "recognition",
                        "id": 4472,
                        "status": "pending_review",
                        "outcome_status": "posting"
                      },
                      "recognition_ids": [
                        4472
                      ],
                      "status": "posting",
                      "message": "Recognition for Priya Nair is posting — it'll appear in the feed in a moment.",
                      "giving_remaining": 750
                    }
                  },
                  "groupGive": {
                    "summary": "Group give — one card, one id per recipient",
                    "value": {
                      "recognition": {
                        "id": 4473,
                        "kind": "recognition",
                        "recipient": {
                          "id": 812,
                          "name": "Priya Nair",
                          "title": "SRE",
                          "image": null
                        },
                        "group_recipients": [
                          {
                            "id": 812,
                            "name": "Priya Nair",
                            "title": "SRE",
                            "image": null
                          },
                          {
                            "id": 907,
                            "name": "Marco Diaz",
                            "title": "Platform Engineer",
                            "image": null
                          }
                        ],
                        "status": "active",
                        "outcome_status": "active"
                      },
                      "recognition_ids": [
                        4473,
                        4474
                      ],
                      "status": "active",
                      "message": "Recognition for Priya Nair and Marco Diaz has been shared!",
                      "giving_remaining": 500
                    }
                  },
                  "withPhoto": {
                    "summary": "With a photo — `photo_url` is the stored rendition, not the URL that was sent",
                    "value": {
                      "recognition": {
                        "id": 4475,
                        "kind": "recognition",
                        "message": "Here's the install she finished single-handed on Saturday.",
                        "status": "active",
                        "outcome_status": "active",
                        "photo_url": "https://officechat.workforce.mangoapps.com/rails/active_storage/representations/redirect/eyJf.../install-day.webp"
                      },
                      "recognition_ids": [
                        4475
                      ],
                      "status": "active",
                      "message": "Recognition for Priya Nair has been shared!",
                      "giving_remaining": 750
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "The caller has no access to the Recognitions app (`access_denied`), or may not give peer recognition at all (`giving_not_allowed`) — the tenant limits giving to managers and Recognition admins."
          },
          "422": {
            "description": "The give was refused. `error.code` says which rule:\n\n| code | meaning |\n|---|---|\n| `no_recipients` | nobody was named, or the caller named only themselves |\n| `invalid_recipients` | none of the ids are people in this business (`details.recipient_ids` echoes them) |\n| `too_many_recipients` | more than `limits.max_recipients` (`details.max_recipients`) |\n| `content_missing` | no message, and no card default message to fall back on |\n| `invalid_photo_url` | `photo_url` could not be turned into a photo — not a public HTTPS URL, not reachable, not a JPG/PNG/WebP/HEIC image, or over 10 MB. `error.message` says which. **Nothing was written**, so the identical give can be retried with a different URL |\n| `governance_blocked` | the anti-gaming guard — the monthly give cap or the duplicate-recipient cooldown. `error.message` names the limit and when it lifts |\n| `content_rejected` | content moderation blocked the message outright |\n| `invalid` | a model validation — message length, a points give with no company value, an exhausted giving allowance |\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionGiveError"
                },
                "examples": {
                  "pointsNeedValue": {
                    "summary": "Points with no company value",
                    "value": {
                      "error": {
                        "code": "invalid",
                        "message": "Could not send recognition: Points require a company value"
                      }
                    }
                  },
                  "tooMany": {
                    "summary": "Over the group cap",
                    "value": {
                      "error": {
                        "code": "too_many_recipients",
                        "message": "You can recognize up to 50 people at once.",
                        "details": {
                          "max_recipients": 50
                        }
                      }
                    }
                  },
                  "governance": {
                    "summary": "Anti-gaming guard",
                    "value": {
                      "error": {
                        "code": "governance_blocked",
                        "message": "You've reached your monthly recognition limit of 20."
                      }
                    }
                  },
                  "photoNotAnImage": {
                    "summary": "photo_url pointed at something that isn't an image",
                    "value": {
                      "error": {
                        "code": "invalid_photo_url",
                        "message": "The photo URL must point to a JPG, PNG, WebP or HEIC image."
                      }
                    }
                  },
                  "photoNotPublic": {
                    "summary": "photo_url is not a public HTTPS URL",
                    "value": {
                      "error": {
                        "code": "invalid_photo_url",
                        "message": "The photo URL must be a public HTTPS image URL."
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The `give_photo` rate limit (error code `rate_limited`) — 20 per minute per person per tenant. It bounds how often this endpoint will issue an OUTBOUND fetch on the caller's behalf, so it is spent ONLY when the request carries `photo_url`; a photo-less give is never rate limited. Nothing is written when it trips. `Retry-After` and `error.details.retry_after_seconds` carry the window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/recognitions/posts/{id}": {
      "get": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Recognition (peer post) detail",
        "description": "Full detail of ONE peer recognition — the screen the feed card opens.\n\nThe feed merges two persisted tables (`RecognitionPost` and `Award`)\nwhose ids collide, so the TYPE lives in the path: this route resolves a\n`RecognitionPost` (\"Recognition\"); awards are fetched via\n`/recognitions/awards/{id}`. Both render through the SAME serializer, so\nthe card body cannot drift from the feed.\n\n**Engagement** carries the comment and reaction COUNTS plus the FULL list\nof every reaction (emoji + who left it) and a grouped `reaction_summary`.\n\n**Permissions** are per-viewer, so the client renders only affordances the\nserver would honour. `can_delete` is true for the author, a business admin,\nor a Recognitions app admin; `can_edit` is the same set of people but only\nwhile the recognition is still ACTIVE (a held or pending post is deletable\nand not editable — see `PATCH`). `can_boost` follows the peer\npile-on rules: the giving allowance is enabled, the viewer is neither the\ngiver nor the recipient, hasn't already boosted, and has points remaining;\nthe `boost` block reports the running total and the affordable amounts.\n\n**Enumeration-safe:** a post the caller may not see under the feed's\nvisibility rules (`Recognition::PostVisibility`) 404s, indistinct from a\nmissing one.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The recognition post id, scoped to the caller's business.",
            "schema": {
              "type": "integer"
            },
            "example": 4471
          }
        ],
        "responses": {
          "200": {
            "description": "The recognition, with engagement, permissions and boost.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "recognition"
                  ],
                  "properties": {
                    "recognition": {
                      "$ref": "#/components/schemas/RecognitionDetail"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "The caller has no access to the Recognitions app (error code `access_denied`)."
          },
          "404": {
            "description": "No such recognition, or one the caller may not see (error code `not_found`). A hidden post is deliberately indistinct from a missing one so ids can't be enumerated."
          }
        }
      },
      "patch": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Edit a recognition (peer post)",
        "description": "The ⋯ ▸ **Edit** action on the recognition detail screen, for a peer\nrecognition. Native mirror of the web edit drawer\n(`RecognitionController#edit_recognition_post` / `#update_recognition_post`)\nand of the mobile edit form — all three run the same service, so the three\nsurfaces cannot drift.\n\n**A typo fix, not a re-give.** Only two fields move:\n\n| field | notes |\n|---|---|\n| `message` | the recognition text. 10–1000 characters. `content` is accepted as an alias. |\n| `company_value_id` | the value tag, from `GET /recognitions/config`'s `values`. |\n\nEverything else is **immutable** and silently ignored if sent —\nrecipient, points, visibility, anonymity, tags, status and the award card.\nThat is the point: an edit must not re-settle points, re-notify the\nrecipient, re-broadcast to Slack/Teams or change who can see the post.\n\n**Partial update.** Only the keys present in the body are written, so a\nbody carrying just `message` leaves the value tag alone. To CLEAR the tag,\nsend `company_value_id` as an empty string — allowed only on a\nrecognition carrying no points (a points-bearing give requires a value,\nthe same rule the give form enforces, so clearing it answers 422).\n\n**Who:** the post's **author** (their own give) OR a **Recognitions\nmoderator** (a business admin or the Recognitions app admin) — the same\nset as delete — and only while the recognition is **active**. A post held\nfor review, pending manager approval, rejected or already removed answers\n**422 `not_editable`** with the reason, deliberately NOT a 403.\n`permissions.can_edit` on the detail GET and on every feed card is this\nexact predicate, so render the Edit entry only when that flag is true.\n\n**Moderation:** edited text goes back through the same AI content screen a\nnew give hits, so an edit can't smuggle in content the create-time check\nwould have caught — a rejection answers 422 `content_rejected` with the\nreason. The screen runs only when the text actually changed (a value-only\nedit makes no LLM call) and fails OPEN, so a provider outage never makes\nrecognitions uneditable.\n\n**Response:** the SAME card `GET /recognitions/posts/{id}` returns, so a\nclient replaces its row from the response instead of re-fetching.\n`unchanged: true` means the body matched what was already stored and\nnothing was written.\n\n`PUT` is accepted on this path with identical (partial-update) semantics.\n\n**Enumeration-safe:** a recognition the caller may neither edit nor even\nsee 404s, indistinct from a missing one. One they can SEE but may not edit\nanswers 403.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The recognition post id, scoped to the caller's business.",
            "schema": {
              "type": "integer"
            },
            "example": 4471
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "properties": {
                  "message": {
                    "type": "string",
                    "minLength": 10,
                    "maxLength": 1000,
                    "description": "The recognition text. Surrounding whitespace is trimmed. `content` is accepted as an alias for clients echoing the column name.",
                    "example": "You carried the whole migration all weekend — thank you again"
                  },
                  "company_value_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "The company value to tag, from `GET /recognitions/config`. Empty string clears the tag (pointless gives only).",
                    "example": 12
                  }
                },
                "example": {
                  "message": "You carried the whole migration all weekend — thank you again",
                  "company_value_id": 12
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The recognition was updated (or already matched the body).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionEdit"
                },
                "examples": {
                  "edited": {
                    "summary": "Message and value updated",
                    "value": {
                      "recognition": {
                        "type": "recognition_post",
                        "id": 4471,
                        "kind": "recognition",
                        "message": "You carried the whole migration all weekend — thank you again",
                        "company_value": "Ownership",
                        "points": 25,
                        "permissions": {
                          "can_edit": true,
                          "can_delete": true,
                          "can_boost": false
                        }
                      },
                      "unchanged": false,
                      "message": "Recognition updated."
                    }
                  },
                  "noop": {
                    "summary": "Nothing changed (nothing was written)",
                    "value": {
                      "unchanged": true,
                      "message": "Recognition updated."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "The caller has no access to the Recognitions app (`access_denied`), or can see this recognition but is neither its author nor a moderator (`forbidden`)."
          },
          "404": {
            "description": "No such recognition in the caller's business, or one they may neither edit nor see (`not_found`)."
          },
          "422": {
            "description": "The recognition isn't editable right now — held, pending, rejected or removed (`not_editable`); the edited text was rejected by content moderation (`content_rejected`); the fields are invalid, e.g. a message under 10 characters or clearing the value on a points-bearing give (a field-level `errors` array); or the save itself failed (`edit_failed`)."
          }
        }
      },
      "put": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Edit a recognition post — alias of PATCH",
        "description": "Alias of `PATCH /recognitions/posts/{id}`, with identical partial-update semantics — fields the body omits are left alone, never cleared. Declared as its own operation so a generated client that only speaks `PUT` gets the method, matching `PUT /recognitions/comments/{id}`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The recognition post id, scoped to the caller's business.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "properties": {
                  "message": {
                    "type": "string",
                    "minLength": 10,
                    "maxLength": 1000
                  },
                  "company_value_id": {
                    "type": "integer",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The recognition was updated (or already matched the body).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionEdit"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "No app access, or neither the post's author nor a moderator."
          },
          "404": {
            "description": "No such recognition, or one they may neither edit nor see."
          },
          "422": {
            "description": "Not editable (`not_editable`), rejected by content moderation (`content_rejected`), invalid fields, or the save failed (`edit_failed`)."
          }
        }
      },
      "delete": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Delete a recognition (peer post)",
        "description": "The ⋯ ▸ **Delete** action on the recognition detail screen, for a peer\nrecognition. Native mirror of the web\n`RecognitionController#destroy_recognition_post` — both run the same\nservice, so the two surfaces cannot drift.\n\n**Who:** the post's **author** (their own give) OR a **Recognitions\nmoderator** (a business admin or the Recognitions app admin). The\n`permissions.can_delete` flag on `GET /recognitions/posts/{id}` and on\nevery feed card is this exact predicate, so the client should render the\nDelete entry only when that flag is true.\n\n**What it does:** a SOFT delete (`status` → `deleted`). The recognition\nleaves the feed and the recipient's profile and the row is retained for\nthe audit trail; the model reverses the points itself — a settled give\nhands the recipient's credited points back and refunds the giver's spent\nallowance, an unsettled one releases the giver's reservation. The author\nis notified, with the optional `reason` included.\n\n**Group gives:** one submission to N recipients is N rows shown as ONE\ncard. This removes only the addressed row — that recipient is\nun-recognized, the rest of the group is untouched — matching the web.\n\n**Idempotent:** deleting an already-deleted recognition succeeds with\n`already_deleted: true` and touches nothing (no second points reversal).\n\n**Enumeration-safe:** a recognition the caller may neither delete nor even\nsee 404s, indistinct from a missing one. A recognition they can SEE but\nmay not delete answers 403, so a client that raced a permission change\ngets a real reason instead of \"gone\".\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The recognition post id, scoped to the caller's business.",
            "schema": {
              "type": "integer"
            },
            "example": 4471
          },
          {
            "name": "reason",
            "in": "query",
            "required": false,
            "description": "Optional note carried into the author's deletion notice — typically supplied when a moderator removes someone else's recognition. May also be sent in a JSON body.",
            "schema": {
              "type": "string",
              "maxLength": 500
            },
            "example": "Posted to the wrong person"
          }
        ],
        "responses": {
          "200": {
            "description": "The recognition was removed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionDeletion"
                },
                "examples": {
                  "deleted": {
                    "summary": "Removed",
                    "value": {
                      "id": 4471,
                      "type": "recognition_post",
                      "deleted": true,
                      "status": "deleted",
                      "already_deleted": false,
                      "message": "Recognition removed.",
                      "unread_notification_count": 3
                    }
                  },
                  "alreadyDeleted": {
                    "summary": "Already removed (idempotent re-issue)",
                    "value": {
                      "id": 4471,
                      "type": "recognition_post",
                      "deleted": true,
                      "status": "deleted",
                      "already_deleted": true,
                      "message": "Recognition was already removed."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "The caller has no access to the Recognitions app (`access_denied`), or can see this recognition but is neither its author nor a moderator (`forbidden`)."
          },
          "404": {
            "description": "No such recognition in the caller's business, or one they may neither delete nor see (`not_found`)."
          },
          "422": {
            "description": "The removal itself failed — points reversal or notification errored (`deletion_failed`)."
          }
        }
      }
    },
    "/recognitions/awards/{id}": {
      "get": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Award / Certificate detail",
        "description": "Full detail of ONE award — the native mirror of the web award permalink\n(RecognitionController#award_show) and its printable certificate.\n\nThis route resolves an `Award`. The user perceives TWO kinds here, both\nserved by this one endpoint and told apart by the `kind` field:\n  * `award` — a human-given program/cycle/nomination award.\n  * `certificate` — an AUTOMATED award (service anniversary, birthday,\n    milestone; `award_metadata.triggered_by` present). Its giver presents\n    as \"System (Automated)\" and it can never be boosted — there is nothing\n    to pile onto on a system-generated certificate. It IS still an Award,\n    so a moderator may revoke or edit one exactly as they may any award;\n    clients typically hide the ⋯ menu on a certificate anyway, and\n    `can_delete` / `can_edit` report the honest server capability rather\n    than that UI choice.\n\nAwards support reactions and comments identically to posts (same\n`engagement` block, same FULL reactions list). Awards are NOT boostable,\nso `boost` is null. `award_art_url` is the gold-framed certificate page.\n\n**Permissions:** awards have no boost (`can_boost` false). Both write\ncapabilities are moderation acts held by the same actor — a Recognitions\nmoderator (a business admin or the Recognitions app admin) while the award\nis still active: `can_delete` maps to the admin REVOKE\n(`DELETE /recognitions/awards/{id}`) and `can_edit` to the citation /\nvalue fix (`PATCH /recognitions/awards/{id}`). Never the giver's own and\nnever the recipient's. There is no age limit on either: the web has never\napplied `Award#can_be_revoked?`'s 30-day window, so neither does this.\n\n**Enumeration-safe:** a non-public award the caller neither gave nor\nreceived (and isn't an admin for) 404s, mirroring\nRecognitionController#load_visible_award.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The award id, scoped to the caller's business.",
            "schema": {
              "type": "integer"
            },
            "example": 991
          }
        ],
        "responses": {
          "200": {
            "description": "The award (or automated certificate), with engagement and permissions.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "recognition"
                  ],
                  "properties": {
                    "recognition": {
                      "$ref": "#/components/schemas/RecognitionDetail"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "The caller has no access to the Recognitions app (error code `access_denied`)."
          },
          "404": {
            "description": "No such award, or one the caller may not see (error code `not_found`)."
          }
        }
      },
      "patch": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Edit a recognition (award / certificate)",
        "description": "The ⋯ ▸ **Edit** action on the recognition detail screen when the card is an\nAward (or an automated Certificate, which is an Award). Runs the same\nservice as the peer-post PATCH above.\n\nEditing an award is a **moderation** act, so — unlike a peer post, which\nits author may fix — this is **moderator only**: a business admin or the\nRecognitions app admin, never the giver's own touch-up and never the\nrecipient's. That is the same actor rule as the revoke on this path,\nbecause an award's citation is program-level copy rather than one person's\nwords. Only an **active** award is editable; a revoked or expired one\nanswers **422 `not_editable`**.\n\n**Two fields move:**\n\n| field | notes |\n|---|---|\n| `message` | the award citation — the `description` column, which is what the card serializes as `message`. 10–1000 characters. `description` is accepted as an alias. |\n| `company_value_id` | the value tag, from `GET /recognitions/config`'s `values`. |\n\nEverything else is **immutable** and silently ignored if sent: `title`,\n`value` (the points/amount), recipient, giver, program, category,\n`is_public` and `status`. An edit therefore never re-credits store points,\nnever redraws a manager's group budget and never changes who can see the\naward.\n\n**Partial update, response and enumeration safety** are exactly as\ndocumented for `PATCH /recognitions/posts/{id}` — only the keys present are\nwritten, the response is the same card the detail GET returns plus\n`unchanged` / `message`, and an award the caller may neither edit nor see\n404s. `PUT` is accepted with identical semantics.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The award id, scoped to the caller's business.",
            "schema": {
              "type": "integer"
            },
            "example": 991
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "properties": {
                  "message": {
                    "type": "string",
                    "minLength": 10,
                    "maxLength": 1000,
                    "description": "The award citation (the `description` column). Surrounding whitespace is trimmed. `description` is accepted as an alias.",
                    "example": "Delivered the Q3 platform migration two weeks ahead of plan"
                  },
                  "company_value_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "The company value to tag, from `GET /recognitions/config`.",
                    "example": 12
                  }
                },
                "example": {
                  "message": "Delivered the Q3 platform migration two weeks ahead of plan"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The award was updated (or already matched the body).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionEdit"
                },
                "examples": {
                  "edited": {
                    "summary": "Citation updated",
                    "value": {
                      "recognition": {
                        "type": "award",
                        "id": 991,
                        "kind": "award",
                        "title": "Excellence in Delivery",
                        "message": "Delivered the Q3 platform migration two weeks ahead of plan",
                        "permissions": {
                          "can_edit": true,
                          "can_delete": true,
                          "can_boost": false
                        }
                      },
                      "unchanged": false,
                      "message": "Recognition updated."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "The caller has no access to the Recognitions app (`access_denied`), or can see this award but is not a Recognitions moderator (`forbidden`) — including its own giver and its recipient."
          },
          "404": {
            "description": "No such award in the caller's business, or one they may neither edit nor see (`not_found`)."
          },
          "422": {
            "description": "The award is no longer editable — revoked or expired (`not_editable`); the edited citation was rejected by content moderation (`content_rejected`); the fields are invalid, e.g. a citation under 10 characters (a field-level `errors` array); or the save itself failed (`edit_failed`)."
          }
        }
      },
      "put": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Edit a recognition (award / certificate) — alias of PATCH",
        "description": "Alias of `PATCH /recognitions/awards/{id}`, with identical partial-update semantics — fields the body omits are left alone, never cleared. Declared as its own operation so a generated client that only speaks `PUT` gets the method, matching `PUT /recognitions/comments/{id}`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The award id, scoped to the caller's business.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "properties": {
                  "message": {
                    "type": "string",
                    "minLength": 10,
                    "maxLength": 1000
                  },
                  "company_value_id": {
                    "type": "integer",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The recognition was updated (or already matched the body).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionEdit"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "No app access, or the caller is not a Recognitions moderator."
          },
          "404": {
            "description": "No such award, or one they may neither edit nor see."
          },
          "422": {
            "description": "Not editable (`not_editable`), rejected by content moderation (`content_rejected`), invalid fields, or the save failed (`edit_failed`)."
          }
        }
      },
      "delete": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Delete a recognition (award / certificate) — admin revoke",
        "description": "The ⋯ ▸ **Delete** action on the recognition detail screen when the card is\nan Award (or an automated Certificate, which is an Award). Native mirror of\nthe web `RecognitionController#revoke_award`.\n\nFor an award, \"delete\" is the admin **REVOKE**: `status` → `revoked`, the\ngiver's group budget is refunded and the recipient's store points are\nreversed. The recipient is notified. `reason` is recorded on the award\n(`award_metadata.revoked_reason`); when omitted it defaults to\n\"Revoked by {caller name}\", so the notice always names a person.\n\n**Who:** a **Recognitions moderator** only — a business admin or the\nRecognitions app admin. Never the giver's own undo and never the\nrecipient's. Only an **active** award can be revoked; there is no age\nlimit (`Award#can_be_revoked?`'s 30-day window has never been enforced on\nthe web, so it isn't enforced here either).\n\nThis is the same predicate `permissions.can_delete` reports on\n`GET /recognitions/awards/{id}`.\n\n**Enumeration-safe:** an award the caller may neither revoke nor see 404s.\nRe-issuing the request on an already-revoked award answers **422\n`not_revocable`** — deliberately NOT a 403, which would be both wrong and\nunactionable.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The award id, scoped to the caller's business.",
            "schema": {
              "type": "integer"
            },
            "example": 991
          },
          {
            "name": "reason",
            "in": "query",
            "required": false,
            "description": "Why the award is being revoked. Stored on the award and included in the recipient's notice. Defaults to \"Revoked by {caller name}\". May also be sent in a JSON body.",
            "schema": {
              "type": "string",
              "maxLength": 500
            },
            "example": "Awarded to the wrong person"
          }
        ],
        "responses": {
          "200": {
            "description": "The award was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionDeletion"
                },
                "examples": {
                  "revoked": {
                    "summary": "Revoked",
                    "value": {
                      "id": 991,
                      "type": "award",
                      "deleted": true,
                      "status": "revoked",
                      "already_deleted": false,
                      "message": "Award revoked. Dana Marsh's points were reversed.",
                      "unread_notification_count": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "The caller has no access to the Recognitions app (`access_denied`), or is not a Recognitions moderator (`forbidden`)."
          },
          "404": {
            "description": "No such award in the caller's business, or one they may neither revoke nor see (`not_found`)."
          },
          "422": {
            "description": "The award is not active, so there is nothing to revoke — `not_revocable` (e.g. it was already revoked or has expired). A revoke that itself failed answers `deletion_failed`."
          }
        }
      }
    },
    "/recognitions/awards/{id}/certificate": {
      "get": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Award certificate — the \"View Certificate\" overlay for one award",
        "description": "One award's gold-framed **Certificate of Recognition** — the native mirror\nof the printable web page (`/recognition/awards/{id}/card`) and the overlay\nbehind the **View Certificate** button on the Award Results screen.\n\nReturns the certificate's fields in the order the printed page renders\nthem: the award title, the recipient, who awarded it, the italic citation\nquote, the program / points / company-value chips, then the footer's\nissuing organization and date. The fixed copy the page prints around those\nfields — the gold eyebrow, \"is proudly presented to\", the \"awarded by\"\nprefix, the QR caption — is in `meta`, so the client hardcodes no strings.\n\n**The same block the results screen embeds.** `certificate` is byte-identical\nto `winners[].certificate` in\n`GET /recognitions/award_cycles/{id}/results`; both come from one\nserializer. A client that already holds the results payload can render the\noverlay from it and refresh from here, and the two can never disagree.\n\n**Why the endpoint exists at all**, given the results payload carries it:\na certificate is reachable from places that hold an **award id and no\ncycle** — its own scan-to-view QR and share link, a push deep link, a My\nRecognition or feed row — and from awards that never came from a cycle at\nall: a manager Quick Award, a nomination award, or an automated lifecycle\ncertificate (an anniversary or milestone, which the feed labels\n\"Certificate\"). This is the award-keyed lookup for all of them.\n\n**Not gated on award cycles.** Certificates are not a Model B feature, and\nthe awards above exist in tenants that never enabled cycles — gating here\nwould 403 the majority of certificates. Access is gated only the way every\nendpoint in this namespace is: the Recognitions app must be enabled for the\ntenant and the caller must be inside its audience.\n\n**Two different URLs, deliberately.** `certificate_url` is the printable\npage for Print / Save; `share_url` is the award permalink — what a Share\naction sends and what the QR should encode. Encoding the print page would\ndead-end a phone that scanned it in a print dialog.\n\n**Opening your own certificate acknowledges it**, silently and with no\nbutton, exactly as both web award surfaces do — the mobile client's\nconfetti stops on the strength of this read. Only the recipient's own read\nwrites anything; a giver, an admin or a colleague opening the same\ncertificate writes nothing.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The **award's** id — `certificate.award_id` on any winner card, or the `id` of any `award` / `certificate` row in the feed. Scoped to the caller's tenant.",
            "example": 23
          }
        ],
        "responses": {
          "200": {
            "description": "The certificate for one award",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "certificate",
                    "meta"
                  ],
                  "properties": {
                    "certificate": {
                      "$ref": "#/components/schemas/RecognitionAwardCycleCertificate"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/RecognitionCertificateMeta"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "The caller's unread notification count, for the app badge.",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Recognitions app is not enabled for the tenant, or this user is outside the app's audience (error code `access_denied`)."
          },
          "404": {
            "description": "No certificate to serve, as one indistinguishable answer (error code\n`not_found`) for all of:\n\n* no award with that id in the caller's tenant\n* an award the caller may not see — the canonical rule is public, or\n  the caller is its recipient or giver, or the caller is an admin\n* an award that is no longer **active** (revoked, expired or deleted),\n  which stops being printable on the web page too — including for its\n  own recipient\n\nThe collapse is deliberate. A distinct 403 would confirm that a\nprivate award with that id exists, which is exactly what a caller\nwalking the id space is trying to learn.\n"
          }
        }
      }
    },
    "/recognitions/comments": {
      "get": {
        "tags": [
          "Recognitions"
        ],
        "summary": "List the comments on a recognition / award / certificate",
        "description": "The comment thread of ONE recognition — the native mirror of the web\nthread (RecognitionController#fetch_comments).\n\nRecognition comments are polymorphic (`RecognitionComment#commentable`),\nso ONE endpoint serves BOTH commentable kinds, selected by `item_type`:\n  * `recognition_post` — a peer recognition post.\n  * `award` — a program/cycle/nomination award.\n  * `certificate` — accepted as an ALIAS of `award` (a certificate is a\n    display variant of an award, not a separate commentable). The response\n    normalizes `item_type` back to `award`.\n\nReturns only ACTIVE comments, oldest-first, paginated. Each comment carries\nits author, content, reaction summary, and per-viewer `can_edit`/`can_delete`\nflags — the SAME predicates the write endpoints enforce, so a client never\nrenders a control the API would refuse.\n\nThreads are ONE level deep. `comments` holds only TOP-LEVEL rows; a reply\nis inlined under the comment it answers, in that comment's `replies` array\n(and carries `parent_id`). A reply is never also a top-level row, so the\nclient draws the thread exactly as returned. `meta.total_count` therefore\ncounts TOP-LEVEL comments — it is the page count for this list, and is\nsmaller than the recognition detail's `comments_count`, which counts every\nvisible row (top-level plus their active replies).\n\n**Enumeration-safe:** a recognition the caller may not see under the feed's\nvisibility rules 404s, indistinct from a missing one — so the thread of a\nprivate/team/department recognition can't be read by guessing ids.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "item_type",
            "in": "query",
            "required": true,
            "description": "The commentable kind — `recognition_post`, `award`, or `certificate` (alias of `award`).",
            "schema": {
              "type": "string",
              "enum": [
                "recognition_post",
                "award",
                "certificate"
              ]
            }
          },
          {
            "name": "item_id",
            "in": "query",
            "required": true,
            "description": "The recognition post / award id, scoped to the caller's business.",
            "schema": {
              "type": "integer"
            },
            "example": 4471
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based page number (default 1).",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Page size (default 20, clamped to 50).",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The active comment thread, oldest-first, paginated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "item_type",
                    "item_id",
                    "comments",
                    "meta"
                  ],
                  "properties": {
                    "item_type": {
                      "type": "string",
                      "enum": [
                        "recognition_post",
                        "award"
                      ],
                      "description": "The canonical commentable type (the `certificate` alias is normalized to `award`)."
                    },
                    "item_id": {
                      "type": "integer",
                      "example": 4471
                    },
                    "comments": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RecognitionComment"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/RecognitionCommentPageMeta"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "The caller has no access to the Recognitions app (error code `access_denied`)."
          },
          "404": {
            "description": "No such recognition, or one the caller may not see (error code `not_found`)."
          },
          "422": {
            "description": "`item_type` was not one of the accepted values (error code `invalid_item_type`)."
          }
        }
      },
      "post": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Add a comment to a recognition / award / certificate",
        "description": "Post a comment on a recognition post or award — the native mirror of\nRecognitionController#comment.\n\n`item_type` selects the commentable (`recognition_post` / `award` /\n`certificate`-as-alias). Content is validated (1–500 characters) and run\nthrough the tenant's content-moderation policy: when the policy HOLDS the\ncomment, it is created hidden pending review and the response returns\n`held: true` so the client can show the same \"submitted for review\" state\nthe web does.\n\n`parent_id` (optional) posts a threaded REPLY to an existing comment on\nthe SAME recognition — the one-level threading the web feed offers.\nA `parent_id` naming a reply is re-pointed at that reply's top-level\nparent (threads never nest deeper); a `parent_id` that names no ACTIVE\ncomment on this recognition is ignored and the text posts as a top-level\ncomment rather than being rejected.\n\nRequires the tenant's comments toggle to be on; otherwise `403`\n(`comments_disabled`).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "item_type",
                  "item_id",
                  "content"
                ],
                "properties": {
                  "item_type": {
                    "type": "string",
                    "enum": [
                      "recognition_post",
                      "award",
                      "certificate"
                    ]
                  },
                  "item_id": {
                    "type": "integer",
                    "example": 4471
                  },
                  "content": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 500,
                    "example": "Fantastic work — well deserved!"
                  },
                  "parent_id": {
                    "type": "integer",
                    "nullable": true,
                    "example": 991,
                    "description": "Optional. The TOP-LEVEL comment on this same recognition that this one replies to. Omit (or send null) for a top-level comment. A reply's own id is re-pointed at its parent — threads are one level deep — and an id that matches no active comment on this recognition is ignored rather than rejected."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created comment (hidden if moderation held it).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "comment",
                    "held"
                  ],
                  "properties": {
                    "comment": {
                      "$ref": "#/components/schemas/RecognitionComment"
                    },
                    "held": {
                      "type": "boolean",
                      "description": "True when the tenant's moderation policy held the comment for review (created hidden)."
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "No access to the Recognitions app (`access_denied`), or comments are disabled for the tenant (`comments_disabled`)."
          },
          "404": {
            "description": "No such recognition, or one the caller may not see (`not_found`)."
          },
          "422": {
            "description": "Blank/too-long content, or an invalid `item_type`."
          }
        }
      }
    },
    "/recognitions/comments/{id}": {
      "patch": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Edit a comment or reply",
        "description": "Edit the text of a recognition comment. **Author-only** — only the person\nwho wrote a comment may reword it (an admin may delete but not rewrite\nsomeone else's words). Editing re-runs content moderation and stamps\n`edited_at`, so the `edited` flag flips true. `PUT` is accepted as an alias\nof `PATCH`.\n\n**Replies are edited by this same route**, addressed by the reply's own\n`id`. Author-only applies to the reply's own author: owning the top-level\ncomment a reply hangs under does NOT confer the right to reword that reply.\n\nOnly `content` is editable. A `parent_id` sent in the body is **ignored** —\na comment cannot be re-parented, so a reply can never be moved under a\ndifferent thread after the fact, and the response's `parent_id` is always\nthe one it was created with.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The comment id, scoped to the caller's business.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "content"
                ],
                "properties": {
                  "content": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 500,
                    "example": "Edited — thank you again!"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated comment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "comment",
                    "held"
                  ],
                  "properties": {
                    "comment": {
                      "$ref": "#/components/schemas/RecognitionComment"
                    },
                    "held": {
                      "type": "boolean"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "The caller is not the comment's author (error code `forbidden`), or has no access to the Recognitions app, or comments are disabled."
          },
          "404": {
            "description": "No such comment in the caller's business, or its recognition is not visible (`not_found`)."
          },
          "422": {
            "description": "Blank/too-long content."
          }
        }
      },
      "put": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Edit a comment (alias of PATCH)",
        "description": "Alias of `PATCH /recognitions/comments/{id}`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "content"
                ],
                "properties": {
                  "content": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 500
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated comment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "comment": {
                      "$ref": "#/components/schemas/RecognitionComment"
                    },
                    "held": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not the author, no app access, or comments disabled."
          },
          "404": {
            "description": "No such comment, or its recognition is not visible."
          },
          "422": {
            "description": "Blank/too-long content."
          }
        }
      },
      "delete": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Delete a comment or reply",
        "description": "Remove a recognition comment. Allowed for the comment's **author** OR a\n**Recognitions moderator** (a business admin or the Recognitions app admin)\n— the same widening the feed uses for `can_delete` on a post.\n\nSoft-deletes (status → `deleted`): the comment leaves the active thread and\nthe recognition's comment count is recomputed, while the row is retained for\nthe audit trail.\n\n**Replies are deleted by this same route**, addressed by the reply's own\n`id`, under the same author-or-moderator rule applied to the reply's own\nauthor. Deleting a reply leaves its parent — and the rest of the thread —\nin place.\n\n**Deleting a top-level comment takes its replies out of view with it.** A\nreply renders only underneath its parent, so once the parent is gone the\nreplies render nowhere: they are omitted from `GET /recognitions/comments`\nand shed from the recognition's `comments_count` along with the parent (one\nparent with one reply drops the count by 2). The reply rows themselves stay\n`active` in the audit trail and are **never promoted to top-level** — so a\nclient must not expect them to reappear as standalone comments.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The comment id, scoped to the caller's business.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The comment was removed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "deleted"
                  ],
                  "properties": {
                    "id": {
                      "type": "integer",
                      "example": 991
                    },
                    "deleted": {
                      "type": "boolean",
                      "example": true
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "The caller is neither the author nor a moderator (error code `forbidden`)."
          },
          "404": {
            "description": "No such comment in the caller's business, or its recognition is not visible (`not_found`)."
          }
        }
      }
    },
    "/recognitions/posts/{id}/boost": {
      "post": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Boost a recognition (pile-on points)",
        "description": "Add **5, 10 or 25 points from your own monthly giving allowance** to\nsomebody else's recognition — the native mirror of the web feed's\n⋯ ▸ **Boost** (`RecognitionController#boost_recognition_post`). Both\nsurfaces run the same `Recognition::BoostService`, so the amounts, the\nallowance spend and the eligibility rules cannot drift.\n\nIt **settles immediately**: the amount leaves the caller's allowance and\nlands in the recipient's store balance, and the recipient is notified.\nThere is no undo — a boost is a one-tap celebration, not a draft.\n\n**Peer recognition posts ONLY.** Awards, certificates and milestone\nrecognitions are not boostable — that is why the path is `/posts/{id}`\nand there is no `/awards/{id}/boost` twin. `GET /recognitions/awards/{id}`\nreports `permissions.can_boost: false` for the same reason.\n\n**Who may call it** — four gates, in the order the server applies them:\n\n| # | Gate | Failure |\n|---|------|---------|\n| 1 | The Recognitions app is accessible to the caller | `403 access_denied` |\n| 2 | The tenant has peer giving points switched on | `403 boosting_disabled` |\n| 3 | The caller may give peer recognition (everyone when the tenant's peer toggle is on; managers and admins always) | `403 forbidden` |\n| 4 | The post exists in the caller's business AND is visible to them | `404 not_found` |\n\nPast those, the service refuses with `422`: your own give, a recognition\nyou received, an amount outside 5/10/25, a second boost on the same post,\nan inactive recipient, or an allowance that can't cover the amount\n(`insufficient_allowance`).\n\n**Don't guess whether the caller may boost** — read the `boost` block on\n`GET /recognitions/feed` or `GET /recognitions/posts/{id}`: `can_boost`\nand `amounts` are computed by the same rules this endpoint enforces, and\n`amounts` is already capped to the caller's remaining allowance.\n\n**Enumeration-safe:** a post in another tenant, or one this caller may not\nsee, returns `404` — indistinct from a missing one, so ids can't be probed\nto discover that a private recognition exists.\n\nThe success payload carries the post's REFRESHED `boost` block and the\ncaller's new `giving_remaining`, so a client patches the card it already\nhas instead of re-fetching the feed. Every `422` carries the same two\nvalues under `error.details`, so a client whose card was stale (someone\nelse's boost landed first, the wallet ran dry in another tab) can\nreconcile straight from the refusal.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The recognition POST id (`RecognitionPost`), scoped to the caller's business.",
            "schema": {
              "type": "integer"
            },
            "example": 4471
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "points"
                ],
                "properties": {
                  "points": {
                    "type": "integer",
                    "enum": [
                      5,
                      10,
                      25
                    ],
                    "description": "The boost amount, spent from the caller's monthly giving allowance. Anything else is refused with `invalid_boost`.",
                    "example": 10
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The boost was applied and settled.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "boost",
                    "recognition",
                    "giving_remaining"
                  ],
                  "properties": {
                    "boost": {
                      "$ref": "#/components/schemas/RecognitionBoostReceipt"
                    },
                    "recognition": {
                      "$ref": "#/components/schemas/RecognitionBoostTarget"
                    },
                    "giving_remaining": {
                      "type": "integer",
                      "description": "The caller's remaining monthly giving allowance AFTER this boost.",
                      "example": 990
                    },
                    "message": {
                      "type": "string",
                      "description": "The same confirmation the web flashes — safe to show verbatim.",
                      "example": "Boosted Dana Wu's recognition with 10 points. You have 990 left this month."
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "No access to the Recognitions app (`access_denied`), the tenant has peer giving points switched off (`boosting_disabled`), or this caller may not give peer recognition (`forbidden`)."
          },
          "404": {
            "description": "No such recognition post in the caller's business, or one they may not see (`not_found`)."
          },
          "422": {
            "description": "The boost was refused — `invalid_boost` (your own give, a recognition you received, an amount outside 5/10/25, an inactive recipient, or you have already boosted this post) or `insufficient_allowance` (your remaining allowance can't cover the amount). Nothing is written and no points move.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionBoostError"
                }
              }
            }
          }
        }
      }
    },
    "/recognitions/reactions": {
      "get": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Who reacted to a recognition",
        "description": "The people who reacted to ONE recognition, most recent first — the\npaginated form of the `engagement.reactions` list that\n`GET /recognitions/posts/{id}` ships inline. Reach for it when a\nrecognition has more reactions than a detail payload should carry, or to\nshow \"everyone who sent 🎉\" with `?emoji=`.\n\n**One polymorphic endpoint for every reactable kind**, keyed by\n`item_type` + `item_id`, exactly as the comment thread is — reactions live\nin `platform_reactions` and the association is polymorphic, so a path per\nkind would be pure duplication.\n\n| `item_type` | Resolves | Notes |\n|---|---|---|\n| `recognition_post` | A peer recognition | |\n| `award` | An award or certificate | |\n| `certificate` | *alias of* `award` | A certificate is an Award rendered with a certificate template |\n| `comment` | A comment on either | |\n\nThe response always echoes the **canonical** `item_type`, so a client that\nposted `certificate` can still key its cache off the response alone.\n\nA client holding a **comment id** can use the comment-scoped twin instead —\n`GET /recognitions/comments/{id}/reactions` — which answers identically\nwithout needing the `(item_type, item_id)` pair.\n\nAlongside the page, the response carries a **`summary`** block: per-emoji\ncounts plus the caller's own glyphs, computed over the **whole set** rather\nthan the page, so a reactor sheet's per-emoji tabs need no second\nround-trip and their counts don't shrink as the reader pages. `?emoji=`\nnarrows the **list** only — never the summary, because the tabs must keep\nshowing the glyphs the reader can switch *to*.\n\n**Not gated on the tenant's reaction switch.** Turning reactions off\nretires the affordance; it does not retract the reactions people already\nleft, and the detail screen keeps showing them. Read `reactions_enabled`\nfrom `GET /recognitions/config` (or from a write response) to decide\nwhether to render the bar.\n\n**Enumeration-safe:** a recognition in another tenant, or one this caller\nmay not see, returns `404` — indistinct from a missing one.\n\nCosts a **constant number of queries** however many people reacted: the\npage is one indexed read plus one batched avatar load.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "item_type",
            "in": "query",
            "required": true,
            "description": "Which kind of recognition the reactions belong to.",
            "schema": {
              "type": "string",
              "enum": [
                "recognition_post",
                "award",
                "certificate",
                "comment"
              ]
            },
            "example": "recognition_post"
          },
          {
            "name": "item_id",
            "in": "query",
            "required": true,
            "description": "The id of that recognition, scoped to the caller's business.",
            "schema": {
              "type": "integer"
            },
            "example": 4471
          },
          {
            "name": "emoji",
            "in": "query",
            "required": false,
            "description": "Narrow the list to a single glyph. Omit for every reaction.",
            "schema": {
              "type": "string"
            },
            "example": "👍"
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Capped at 100.",
            "schema": {
              "type": "integer",
              "default": 25,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The reactions on this recognition, most recent first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "item_type",
                    "item_id",
                    "reactions",
                    "summary",
                    "meta"
                  ],
                  "properties": {
                    "item_type": {
                      "type": "string",
                      "enum": [
                        "recognition_post",
                        "award",
                        "recognition_comment"
                      ],
                      "description": "The CANONICAL type, even when the request used an alias."
                    },
                    "item_id": {
                      "type": "integer",
                      "example": 4471
                    },
                    "reactions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RecognitionReactor"
                      }
                    },
                    "summary": {
                      "$ref": "#/components/schemas/RecognitionReactionSummary"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/RecognitionReactionsPage"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "No access to the Recognitions app (`access_denied`)."
          },
          "404": {
            "description": "No such recognition in the caller's business, or one they may not see (`not_found`)."
          },
          "422": {
            "description": "`item_type` is not a recognition kind (`invalid_item_type`). `error.details.allowed` lists the ones that are."
          }
        }
      },
      "post": {
        "tags": [
          "Recognitions"
        ],
        "summary": "React to a recognition (toggle)",
        "description": "**Capture a reaction** on a recognition post, an award/certificate, or a\ncomment on either — the native mirror of the web reaction bar\n(`POST /recognition/reactions/toggle` → `RecognitionController#react`).\nBoth surfaces run the same `Recognition::ReactionService`, so the tenant\nswitch, the accepted kinds, the audience gate, the emoji allowlist and the\nrate limit cannot drift.\n\n**TOGGLE semantics**, matching the web bar and\n`Platform::Reactable#toggle_reaction`: re-sending an emoji the caller\nalready left **removes** it; otherwise it adds it. `reacted` says which way\nthis call went, and the status line says it too — **`201` when a reaction\nwas added, `200` when one was removed**. A viewer may hold several\n*different* emojis at once; each is its own row, toggled independently.\nWhen you need \"off, whatever the current state is\", use `DELETE` — it is\nidempotent and a retry can never flip the reaction back on.\n\n**Send an `Idempotency-Key` header on a retry-prone connection.** Without\none, a retried POST toggles a second time and silently undoes the first.\nWith one, the stored response is replayed (`X-Idempotency-Cached: true`)\nand nothing is written twice.\n\n**The emoji allowlist is per model** — `Platform::Reactable`'s\n`reactable_emoji_set`. Recognition posts, awards and recognition comments\nall accept **👍 ❤️ 🎉 👏 ⭐ 🔥**; anything else is refused with\n`invalid_emoji`, and `error.details.allowed` carries the set so a client\ncan correct itself without a second round-trip.\n\n**Who may call it** — four gates, in the order the server applies them:\n\n| # | Gate | Failure |\n|---|------|---------|\n| 1 | The Recognitions app is accessible to the caller | `403 access_denied` |\n| 2 | The caller is inside the shared 30/minute `react` bucket — the SAME counter the web bar spends, so a client can't out-run the web limit by switching surface | `429 rate_limited` (with `Retry-After`) |\n| 3 | The tenant has reactions switched on | `403 reactions_disabled` |\n| 4 | The recognition exists in the caller's business AND is visible to them | `404 not_found` |\n\n**Enumeration-safe:** a recognition in another tenant, a private give the\ncaller isn't part of, and a missing id all return the same `404`.\n\nThe success payload carries the recognition's **refreshed reaction block**,\nkeyed exactly as `engagement` is keyed on `GET /recognitions/feed` and\n`GET /recognitions/posts/{id}` — so a client patches the card it already\nhas instead of re-fetching the screen. It is computed with two aggregate\nqueries, so a recognition with a thousand reactions costs the same as one\nwith three.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "A client-generated key. A retry carrying the same key replays the stored response instead of toggling again.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "item_type",
                  "item_id",
                  "emoji"
                ],
                "properties": {
                  "item_type": {
                    "type": "string",
                    "enum": [
                      "recognition_post",
                      "award",
                      "certificate",
                      "comment"
                    ],
                    "description": "Which kind of recognition to react to. `certificate` aliases `award`; `comment` resolves a comment on either. The web bar's class-name spellings (`RecognitionPost`, `Award`, `RecognitionComment`) are accepted too.",
                    "example": "recognition_post"
                  },
                  "item_id": {
                    "type": "integer",
                    "description": "The id of that recognition, scoped to the caller's business.",
                    "example": 4471
                  },
                  "emoji": {
                    "type": "string",
                    "enum": [
                      "👍",
                      "❤️",
                      "🎉",
                      "👏",
                      "⭐",
                      "🔥"
                    ],
                    "description": "The glyph to toggle. Must be in the target model's `reactable_emoji_set`.",
                    "example": "🎉"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The reaction was ADDED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionReactionState"
                }
              }
            }
          },
          "200": {
            "description": "The caller already held that emoji, so the toggle REMOVED it (`reacted: false`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionReactionState"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "No access to the Recognitions app (`access_denied`), or the tenant has reactions switched off (`reactions_disabled`). Nothing is written."
          },
          "404": {
            "description": "No such recognition in the caller's business, or one they may not see (`not_found`)."
          },
          "422": {
            "description": "The reaction was refused — `invalid_item_type` (not a recognition kind), `emoji_required` (blank), or `invalid_emoji` (outside the model's allowlist). `error.details.allowed` carries the accepted values. Nothing is written."
          },
          "429": {
            "description": "Past the shared 30/minute `react` bucket (`rate_limited`). The `Retry-After` header and `error.details.retry_after_seconds` both carry the window. Nothing is written."
          }
        }
      },
      "delete": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Remove your reaction (idempotent)",
        "description": "Remove the caller's own `emoji` from one recognition — the unambiguous half\nof the toggle above.\n\n**Idempotent.** Removing a reaction that isn't there succeeds with\n`removed: false`, so a client retrying a dropped request never flips the\nreaction back on the way a repeated `POST` would. This is the call to use\nwhen your UI knows the target state (\"off\") rather than the transition.\n\n**Scoped to the caller's own row** — it can never remove somebody else's\nreaction, and it leaves the caller's *other* emojis on the same recognition\nalone.\n\nSame four gates as `POST`, in the same order, including the shared\n30/minute `react` bucket. Answers with the same refreshed reaction block,\nplus `removed`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "item_type",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "recognition_post",
                "award",
                "certificate",
                "comment"
              ]
            },
            "example": "recognition_post"
          },
          {
            "name": "item_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "example": 4471
          },
          {
            "name": "emoji",
            "in": "query",
            "required": true,
            "description": "The glyph to remove.",
            "schema": {
              "type": "string"
            },
            "example": "🎉"
          }
        ],
        "responses": {
          "200": {
            "description": "The caller holds no such reaction any more. `removed` says whether this call is what removed it.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/RecognitionReactionState"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "removed": {
                          "type": "boolean",
                          "description": "True when this call deleted a row; false when there was nothing to delete (still a success — the endpoint is idempotent).",
                          "example": true
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "No access to the Recognitions app (`access_denied`), or the tenant has reactions switched off (`reactions_disabled`)."
          },
          "404": {
            "description": "No such recognition in the caller's business, or one they may not see (`not_found`)."
          },
          "422": {
            "description": "`invalid_item_type`, `emoji_required`, or `invalid_emoji`."
          },
          "429": {
            "description": "Past the shared 30/minute `react` bucket (`rate_limited`)."
          }
        }
      }
    },
    "/recognitions/comments/{id}/reactions": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The comment's id, scoped to the caller's business. A comment on a peer recognition, on an award/certificate, or a reply — a reply is just a comment with a parent and reacts on this same path.",
          "schema": {
            "type": "integer"
          },
          "example": 88213
        }
      ],
      "get": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Who reacted to a comment",
        "description": "**Fetch the reactions on ONE comment** in a recognition thread, most\nrecent first — the comment-scoped twin of\n`GET /recognitions/reactions`, in the shape the News Feed API uses for the\nsame case (`GET /comments/{id}/reactions`). Reach for it when you already\nhold a comment id: a client rendering a thread has comment ids, not\n`(item_type, item_id)` pairs.\n\nIt is a **path, not a second implementation.** This endpoint and\n`/recognitions/reactions` run the same action body over the same\n`Recognition::ReactionService` the **web comment row's reaction bar** runs\n(`shared/recognition/_feed_comment` → `shared/_platform_reaction_bar` →\n`POST /recognition/reactions/toggle`), so the three surfaces cannot\ndisagree. Passing `?item_type=comment&item_id={id}` to\n`/recognitions/reactions` returns a byte-identical body.\n\nAlongside the page it carries a **`summary`** block — per-emoji counts plus\nthe caller's own glyphs — computed over the **whole set** rather than the\npage, so a reactor sheet's per-emoji tabs need no second round-trip and\ntheir counts don't shrink as the reader pages. `?emoji=` narrows the\n**list** only, never the summary: the tabs must keep showing the glyphs the\nreader can switch *to*.\n\n**Not gated on the tenant's reaction switch.** Turning reactions off\nretires the affordance; it does not retract what people already left, and\nthe web thread keeps showing them. `summary.reactions_enabled` reports the\nswitch so a client reading only this endpoint still knows to hide the bar.\n\n**The audience gate is the comment's PARENT recognition** — the same rule\nthe web thread applies. A comment in another tenant, one hanging off a\nprivate recognition this caller isn't part of, and a missing id all return\nthe same `404`, so ids can't be enumerated to discover a private thread.\n\nCosts a **constant number of queries** however many people reacted: one\nindexed page read, one batched avatar load, two aggregates for the summary.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "emoji",
            "in": "query",
            "required": false,
            "description": "Narrow the LIST to a single glyph. Omit for every reaction.",
            "schema": {
              "type": "string"
            },
            "example": "👍"
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Capped at 100.",
            "schema": {
              "type": "integer",
              "default": 25,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The reactions on this comment, most recent first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "item_type",
                    "item_id",
                    "reactions",
                    "summary",
                    "meta"
                  ],
                  "properties": {
                    "item_type": {
                      "type": "string",
                      "enum": [
                        "recognition_comment"
                      ],
                      "description": "Always `recognition_comment` — the kind is fixed by the path."
                    },
                    "item_id": {
                      "type": "integer",
                      "description": "The comment's id, echoing the path.",
                      "example": 88213
                    },
                    "reactions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RecognitionReactor"
                      }
                    },
                    "summary": {
                      "$ref": "#/components/schemas/RecognitionReactionSummary"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/RecognitionReactionsPage"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "No access to the Recognitions app (`access_denied`)."
          },
          "404": {
            "description": "No such comment in the caller's business, or one whose parent recognition they may not see (`not_found`)."
          }
        }
      },
      "post": {
        "tags": [
          "Recognitions"
        ],
        "summary": "React to a comment (toggle)",
        "description": "**Capture a reaction** on one comment in a recognition thread — the native\nmirror of the web comment row's own reaction bar\n(`shared/recognition/_feed_comment` renders `shared/_platform_reaction_bar`\nwith the comment as the reactable, posting to\n`POST /recognition/reactions/toggle`). Both surfaces run the same\n`Recognition::ReactionService`, so the tenant switch, the audience gate,\nthe emoji allowlist and the rate limit cannot drift.\n\n**TOGGLE semantics**, matching the web bar: re-sending an emoji the caller\nalready left **removes** it; otherwise it adds it. `reacted` says which way\nthis call went, and the status line says it too — **`201` when a reaction\nwas added, `200` when one was removed**. A viewer may hold several\n*different* emojis on the same comment, each toggled independently. When\nyou need \"off, whatever the current state is\", use `DELETE`.\n\n**Send an `Idempotency-Key` header on a retry-prone connection.** Without\none, a retried POST toggles a second time and silently undoes the first.\nWith one, the stored response is replayed (`X-Idempotency-Cached: true`).\n\nComments accept **👍 ❤️ 🎉 👏 ⭐ 🔥**. Anything else is refused with\n`invalid_emoji`, and `error.details.allowed` carries the set.\n\n**Who may call it** — four gates, in the order the server applies them:\n\n| # | Gate | Failure |\n|---|------|---------|\n| 1 | The Recognitions app is accessible to the caller | `403 access_denied` |\n| 2 | The caller is inside the shared 30/minute `react` bucket — ONE counter across this path, `/recognitions/reactions` and the web bar, so a client can't out-run the limit by switching path | `429 rate_limited` (with `Retry-After`) |\n| 3 | The tenant has reactions switched on | `403 reactions_disabled` |\n| 4 | The comment exists in the caller's business AND its parent recognition is visible to them | `404 not_found` |\n\nNote gate 4 is about **reactions**, not comments: turning the tenant's\n*comments* switch off stops new comments, it does not freeze the reactions\non a thread already there — exactly as the web row behaves.\n\nThe success payload carries the comment's **refreshed reaction block**,\nkeyed exactly as `reaction_summary` is keyed everywhere else in this\nnamespace, so a client patches the comment row it already holds instead of\nre-fetching the thread. Two aggregate queries, so a comment with a thousand\nreactions costs the same as one with three.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "A client-generated key. A retry carrying the same key replays the stored response instead of toggling again.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emoji"
                ],
                "properties": {
                  "emoji": {
                    "type": "string",
                    "enum": [
                      "👍",
                      "❤️",
                      "🎉",
                      "👏",
                      "⭐",
                      "🔥"
                    ],
                    "description": "The glyph to toggle.",
                    "example": "👏"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The reaction was ADDED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionReactionState"
                }
              }
            }
          },
          "200": {
            "description": "The caller already held that emoji, so the toggle REMOVED it (`reacted: false`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionReactionState"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "No access to the Recognitions app (`access_denied`), or the tenant has reactions switched off (`reactions_disabled`). Nothing is written."
          },
          "404": {
            "description": "No such comment in the caller's business, or one whose parent recognition they may not see (`not_found`)."
          },
          "422": {
            "description": "`emoji_required` (blank) or `invalid_emoji` (outside the allowlist). `error.details.allowed` carries the accepted glyphs. Nothing is written."
          },
          "429": {
            "description": "Past the shared 30/minute `react` bucket (`rate_limited`). The `Retry-After` header and `error.details.retry_after_seconds` both carry the window. Nothing is written."
          }
        }
      },
      "delete": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Remove your reaction from a comment (idempotent)",
        "description": "Remove the caller's own `emoji` from one comment — the unambiguous half of\nthe toggle above.\n\n**Idempotent.** Removing a reaction that isn't there succeeds with\n`removed: false`, so a client retrying a dropped request never flips the\nreaction back on the way a repeated `POST` would. This is the call to use\nwhen your UI knows the target state (\"off\") rather than the transition.\n\n**Scoped to the caller's own row** — it can never remove somebody else's\nreaction, and it leaves the caller's *other* emojis on the same comment\nalone.\n\nSame four gates as `POST`, in the same order, including the shared\n30/minute `react` bucket. Answers with the same refreshed reaction block,\nplus `removed`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "emoji",
            "in": "query",
            "required": true,
            "description": "The glyph to remove.",
            "schema": {
              "type": "string"
            },
            "example": "👏"
          }
        ],
        "responses": {
          "200": {
            "description": "The caller holds no such reaction any more. `removed` says whether this call is what removed it.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/RecognitionReactionState"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "removed": {
                          "type": "boolean",
                          "description": "True when this call deleted a row; false when there was nothing to delete (still a success — the endpoint is idempotent).",
                          "example": true
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "No access to the Recognitions app (`access_denied`), or the tenant has reactions switched off (`reactions_disabled`)."
          },
          "404": {
            "description": "No such comment in the caller's business, or one whose parent recognition they may not see (`not_found`)."
          },
          "422": {
            "description": "`emoji_required` or `invalid_emoji`."
          },
          "429": {
            "description": "Past the shared 30/minute `react` bucket (`rate_limited`)."
          }
        }
      }
    },
    "/recognitions/posts/{id}/acknowledge": {
      "post": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Settle a recognition celebration (peer recognition)",
        "description": "**\"I've celebrated this one.\"** `GET /api/v1/apps` carries\n`unacknowledged_recognitions` on the Recognitions node — the recognitions\nthe caller has RECEIVED and not yet seen. The client fires its confetti on\napp open and then settles each item here, so the next launch is quiet.\n\n**The implicit half needs no call.** Opening a recognition's detail\nalready acknowledges it (`GET /recognitions/posts/{id}`, and the web +\nmobile-web detail screens) — silently, reporting nothing. Use this path\nfor the case with no detail view in it: the app celebrated on the launcher\nand the user never tapped through.\n\n**Silent.** Acknowledging writes one timestamp: no notification, no feed\nchange, no `updated_at` bump, nothing the giver or anyone else can\nobserve.\n\n**NOT-THE-RECIPIENT IS A `200`, NOT AN ERROR.** A colleague — or a\nmoderator, or the giver — calling this on a recognition they can see gets\n`acknowledged: false` with a diagnostic `reason` and no write. A\nrecognition the caller may not SEE still `404`s, exactly as the detail GET\ndoes, so ids can't be enumerated to discover a private give.\n\n**Group gives resolve to the caller's OWN row.** One submission naming\nfive people is five rows sharing a `recognition_group_id`, and every\nsurface collapses them to one card — so four of those five people hold an\nid whose recipient is somebody else. This endpoint stamps the caller's own\nsibling row, and the echoed `id` is the row that was stamped (which may\ndiffer from the `id` in the path).\n\n**One item per call, and idempotent.** A repeat call answers\n`acknowledged: true, newly_acknowledged: false` — `newly_acknowledged` is\nthe flag a client keys the animation off, so re-opening never\nre-celebrates. There is deliberately no bulk path: a client settling two\nor three celebrations makes two or three calls.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The recognition post's id, scoped to the caller's business.",
            "schema": {
              "type": "integer"
            },
            "example": 4471
          }
        ],
        "responses": {
          "200": {
            "description": "The acknowledgement state after this call. `acknowledged: false` when the caller is not the person recognized — an ordinary no-op, not an error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionAcknowledgement"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "No access to the Recognitions app (`access_denied`)."
          },
          "404": {
            "description": "No such recognition in the caller's business, or one they may not see (`not_found`)."
          }
        }
      }
    },
    "/recognitions/awards/{id}/acknowledge": {
      "post": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Settle a recognition celebration (award / certificate)",
        "description": "The award half of `POST /recognitions/posts/{id}/acknowledge` — same rule,\nsame silence, same `200`-for-a-non-recipient contract, same idempotency.\n\nSplit by type because `RecognitionPost` and `Award` ids collide, exactly as\nthe detail GETs are split (`GET /recognitions/posts/{id}` vs\n`GET /recognitions/awards/{id}`). The `type` in the response is the one the\npending list emits, so the `type` a client is given is the `type` it reads\nback.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The award's id, scoped to the caller's business.",
            "schema": {
              "type": "integer"
            },
            "example": 812
          }
        ],
        "responses": {
          "200": {
            "description": "The acknowledgement state after this call. `acknowledged: false` when the caller is not the person recognized — an ordinary no-op, not an error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecognitionAcknowledgement"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "No access to the Recognitions app (`access_denied`)."
          },
          "404": {
            "description": "No such award in the caller's business, or one they may not see (`not_found`)."
          }
        }
      }
    },
    "/company-store/approvals": {
      "get": {
        "tags": [
          "Company Store"
        ],
        "summary": "Redemption approval queue",
        "description": "The Approvals tab: every redemption held for this caller's decision,\npaginated, plus the badge figure a client needs beside the tab.\n\n**Approvers and store admins only** — see the authorization note on this\nfile's other operations and the four gates listed there. A caller who\nholds no queue gets `403 not_an_approver` (or `403 approvals_delegated`\nwhen a designated approver group has superseded their org-chart route),\nnever an empty queue.\n\n### The pool\n\n`Store::RedemptionApprovalService.approval_queue_for` — the ONE\ndefinition, which picks between two shared pools on the same precedence\n`.can_approve?` itself applies:\n\n* **store admin** → `.admin_queue_for`: every `pending_approval` order in\n  the tenant, at **every** approval tier. Shared verbatim with the admin\n  orders page's \"Redemptions Awaiting Approval\" section, so the two lists\n  cannot drift.\n* **everyone else** → `.manager_queue_for`: `pending_approval` orders\n  routed to the **manager** tier only, narrowed to the caller's own\n  reports (or the whole tenant for an approver-group member). Shared with\n  the web queue page, the web dashboard's pending-count banner and the\n  native dashboard's Team Approvals widget.\n\nThree private copies of \"the manager queue\" is exactly how the dashboard\nonce counted holds the queue page then refused, so nothing about either\npool is re-derived here.\n\n### `total_pending` — the badge figure\n\nThe FULL queue depth, independent of `page` / `per_page`, on **every**\nresponse including the two decision endpoints. So a client renders its tab\nbadge from the same call that drew the list, and the badge updates from\nthe same response that took a decision — no second request, and no window\nwhere the badge disagrees with the screen.\n\n### Ordering\n\n`sort=oldest` (the default) is longest-waiting first — the order a queue\nshould be worked in, and what the dashboard widget shows; those are the\nrequests an approver is holding up. `sort=newest` matches the web table.\nAn unrecognised value falls back to the default rather than erroring.\n\n### `scope`\n\n`company` for a store admin or a designated approver-group member (their\nqueue spans the tenant) or `team` for a line manager (their own reports).\nThe web page renders two different subtitles off exactly this distinction\n— calling a committee's rows \"your team\" mislabelled every one of them.\n\n### Cost\n\nFlat per page. The requester, their department, the item and both\nattachment chains (requester avatar, product thumbnail) are batched in one\npass, so a page of 50 rows costs the same queries as a page of 2. The\nper-row `can_approve` costs at most ONE extra query for the whole page —\nand none at all unless the caller is a Company Store app admin who is not\na business admin, the only persona whose pool is wider than their\napprovable set.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            },
            "description": "1-based page number. `0` / negative / junk is treated as page 1."
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            },
            "description": "Rows per page, clamped to 50 server-side — an unbounded `per_page` is how one client turns a paginated endpoint into a full-table read. A non-positive or junk value falls back to 20. `filters.per_page` reports what was actually applied."
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "oldest",
                "newest"
              ],
              "default": "oldest"
            },
            "description": "`oldest` (default) — longest-waiting first, the order a queue is worked in and what the dashboard's Team Approvals widget uses. `newest` — the web table's own order. An unrecognised value falls back to `oldest`, and `filters.sort` reports which was applied."
          }
        ],
        "responses": {
          "200": {
            "description": "Queue retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "approvals": {
                      "$ref": "#/components/schemas/CompanyStoreApprovalQueue"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "Piggybacked on every native response for badge management."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token"
          },
          "403": {
            "description": "`insufficient_permissions` (the token lacks `read:company_store` — checked first, before any of the gates below), `access_denied` (Company Store not accessible to this user), `store_disabled` (the tenant paused the store), `approvals_delegated` (a designated approver group has superseded this line manager) or `not_an_approver` (this caller has no approval queue).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreApprovalError"
                }
              }
            }
          }
        }
      }
    },
    "/company-store/approvals/{id}/approve": {
      "post": {
        "tags": [
          "Company Store"
        ],
        "summary": "Approve a held redemption",
        "description": "Releases the hold for fulfillment and notifies the employee — the native\nequivalent of the web queue's **Approve** button.\n\nRuns `Store::RedemptionApprovalService.approve!`, the same call the web\nbutton makes, which flips the order to `pending`, records the approver on\nthe order, dispatches fulfillment (gift cards fulfill inline; physical /\nprint-on-demand enqueue to the provider) and notifies the redeemer\nin-app/push. **The points stay spent** — `points_refunded` is `0`.\n\nOnly for a hold in **this** caller's own queue: an `id` they cannot even\nsee answers `404`, not `403`, because from this endpoint's point of view\nit is not an approvable request at all. A hold they CAN see but may not\ndecide — the admin branch's `can_approve: false` rows — answers `403\nnot_an_approver`.\n\nThe response carries the recomputed `total_pending`, so a client's tab\nbadge updates from the same call that took the decision.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The StoreOrder id, as reported by each queue row's `id` (and by the dashboard widget's `team_approvals.requests[].id`) — the same identifier the web queue's own buttons post to. Digits only."
          }
        ],
        "responses": {
          "200": {
            "description": "Approved and released for fulfillment",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreApprovalDecisionResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token"
          },
          "403": {
            "description": "`insufficient_permissions` (the token lacks `write:company_store`), `access_denied`, `store_disabled`, `approvals_delegated` or `not_an_approver` — see the queue endpoint. `not_an_approver` also covers the per-ROW refusal: the caller holds a queue and this hold is in it, but they may not decide THIS one (`can_approve: false`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreApprovalError"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — that redemption isn't in your approval queue (it was never held, it is held at a different tier, or it belongs to somebody outside your team).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreApprovalError"
                }
              }
            }
          },
          "409": {
            "description": "`decision_failed` — the hold moved out from under you between drawing the queue and deciding (already approved or declined by someone else, or re-tiered so it now routes to a different approver). The request was well-formed and you ARE an approver, so a client should refresh the queue rather than retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreApprovalError"
                }
              }
            }
          }
        }
      }
    },
    "/company-store/approvals/{id}/decline": {
      "post": {
        "tags": [
          "Company Store"
        ],
        "summary": "Decline a held redemption",
        "description": "Declines the redemption, **refunds the held points to the employee** and\nrestocks the item — the native equivalent of the web queue's **Decline\nredemption** modal.\n\nRuns `Store::RedemptionApprovalService.reject!`, the same call the web\nmodal makes: the order is cancelled (which returns the points to the\nemployee's wallet, and to the team store budget it was drawn from if any,\nand increments the item's inventory back) and the employee is notified.\n`points_refunded` reports what went back.\n\n`reason` is **optional but employee-facing**: the redeemer sees it in\ntheir notification, so send the one the approver typed. With none, the\nservice records a generic \"not approved by &lt;approver&gt;\". A\nwhitespace-only reason is treated as none given. (The web modal requires a\nreason before enabling its submit button; the native reject sheet has\nnone, so the server accepts both.)\n\nOnly for a hold in **this** caller's own queue — same `403` / `404` /\n`409` semantics as `approve`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The StoreOrder id, as reported by each queue row's `id` (and by the dashboard widget's `team_approvals.requests[].id`) — the same identifier the web queue's own buttons post to. Digits only."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "description": "Why it was declined. The EMPLOYEE sees this — send the approver's own words. Optional; blank/absent records a generic \"not approved by <approver>\".",
                    "example": "This item is out of budget for this quarter"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Declined; points refunded and inventory restocked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreApprovalDecisionResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token"
          },
          "403": {
            "description": "`insufficient_permissions` (the token lacks `write:company_store`), `access_denied`, `store_disabled`, `approvals_delegated` or `not_an_approver` — see the queue endpoint. `not_an_approver` also covers the per-ROW refusal: the caller holds a queue and this hold is in it, but they may not decide THIS one (`can_approve: false`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreApprovalError"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — that redemption isn't in your approval queue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreApprovalError"
                }
              }
            }
          },
          "409": {
            "description": "`decision_failed` — already decided, or re-routed to a different approver.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStoreApprovalError"
                }
              }
            }
          }
        }
      }
    },
    "/libraries/list": {
      "get": {
        "tags": [
          "Libraries"
        ],
        "summary": "All Libraries (index)",
        "description": "The native-client mirror of the web **All Libraries** index\n(`Apps::LibrariesController#show`), shaped to the Libraries design\nmockup's home screen. One row carries everything a card needs — cover,\nmark, kind, both counts, last-updated and the two capability flags — so\nrendering a screen costs ONE call and no per-row follow-ups.\n\nBoth surfaces resolve the list through the same `Libraries::IndexQuery`,\nso the API and the web page cannot drift on which libraries are visible,\nwhat \"Admin order\" means, or where disabled libraries sit.\n\n### What the caller sees\n\n* **A Libraries admin** (business admin, super admin, or a per-app\n  Libraries admin) sees every library in the tenant, **disabled ones\n  included** — they are badged, not hidden, because this is the surface\n  that offers Enable. `can_disable` is `true`.\n* **Everyone else** sees only ENABLED libraries whose audience admits\n  them: `visibility: all_users` libraries, plus any whose audience rules\n  match them by user, department, group, location, job family, job title,\n  organizational role or platform role. A disabled library is never\n  visible to them under any filter. `can_disable` is `false`.\n\nVisibility narrows the rows, `filter_counts` AND `meta.total_count`\ntogether, so every chip badge counts exactly what clicking it returns.\n\n### Ordering\n\n**Disabled libraries are ALWAYS LAST, under every sort** — they are\nadmin-only, and a hidden library taking a slot among the live ones pushes\nreal content down the page. The requested `sort` orders rows *within* the\nenabled and disabled groups. The rule lives in the SQL `ORDER BY`, so it\nholds across page boundaries: disabled rows land on the last page, never\nat the bottom of page 1.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "filter",
            "in": "query",
            "required": false,
            "description": "Which chip is selected. `disabled` is ADMIN-ONLY — a disabled library is invisible to everyone else, so a member requesting it falls back to `all` (200, with `active_filter: \"all\"`) rather than being refused. An unknown value also falls back to `all`. Read `available_filters` to know which chips to draw.",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "disabled"
              ],
              "default": "all"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "The order to list in — the mockup's three options:\n\n* `admin` — **Admin order**: the order admins set in the web Reorder\n  dialog (the stored `position`), then name.\n* `az` — **A → Z**: by name ascending, case-insensitively.\n* `recent` — **Recently updated**: newest `updated_at` first. This is\n  the same timestamp the card's `updated_at` carries, so the sort and\n  the label can never disagree.\n\n`updated` is accepted as a DEPRECATED alias of `recent` (the mobile\nprototype's spelling); the response always echoes the **canonical**\nkey in `sort`, so a client can tell which order it actually got. An\nunknown value falls back to `admin`.\n",
            "schema": {
              "type": "string",
              "enum": [
                "admin",
                "az",
                "recent"
              ],
              "default": "admin"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based. A missing, zero, negative, non-numeric or collection-shaped value is 1. A page past the end is an empty 200, not an error.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Rows per page, capped at 50. A value that does not parse as a positive integer (`0`, `-3`, `abc`, `?per_page[]=5`) is treated as ABSENT and falls back to 20 — deliberately NOT floored to 1, which would hand a malformed client a one-row-per-page walk of the whole list.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Library list retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "libraries",
                    "active_filter",
                    "sort",
                    "available_filters",
                    "available_sorts",
                    "filter_counts",
                    "can_disable",
                    "meta"
                  ],
                  "properties": {
                    "libraries": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LibraryCard"
                      }
                    },
                    "active_filter": {
                      "type": "string",
                      "enum": [
                        "all",
                        "disabled"
                      ],
                      "description": "The filter actually applied — which is not always the one requested (an unknown value, or `disabled` from a member, resolves to `all`).",
                      "example": "all"
                    },
                    "sort": {
                      "type": "string",
                      "enum": [
                        "admin",
                        "az",
                        "recent"
                      ],
                      "description": "The order actually applied, always as the CANONICAL key (a `?sort=updated` request answers `recent`).",
                      "example": "admin"
                    },
                    "available_filters": {
                      "type": "array",
                      "description": "The chips THIS caller may select — `[\"all\", \"disabled\"]` for a Libraries admin, `[\"all\"]` for everyone else. Draw the chip row from this rather than hard-coding the admin-only rule.",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "all",
                        "disabled"
                      ]
                    },
                    "available_sorts": {
                      "type": "array",
                      "description": "The three orders, in the order the menu shows them.",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "admin",
                        "az",
                        "recent"
                      ]
                    },
                    "filter_counts": {
                      "type": "object",
                      "description": "Total per available filter, for the chip badges. Counted over the SAME visible set the rows come from, so each badge equals the `meta.total_count` that filter returns. A non-admin payload OMITS the `disabled` key entirely (not a zero) — that chip was never offered.",
                      "properties": {
                        "all": {
                          "type": "integer",
                          "example": 8
                        },
                        "disabled": {
                          "type": "integer",
                          "description": "Admin only. Absent for other callers.",
                          "example": 2
                        }
                      }
                    },
                    "can_disable": {
                      "type": "boolean",
                      "description": "Whether this caller may Disable / Enable a library — the gate behind the row menu's Disable option and behind `POST /libraries/{id}/disable`. App-wide, not per-library, so it is also mirrored onto every card.",
                      "example": true
                    },
                    "meta": {
                      "$ref": "#/components/schemas/LibraryListMeta"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "Piggybacked on every response in this API for native badge management. Unrelated to Libraries.",
                      "example": 3
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API token"
          },
          "403": {
            "description": "`insufficient_permissions` — the token lacks `read:libraries`; or `access_denied` — the Libraries app is not enabled for the tenant, or this user is outside the app's audience."
          }
        }
      }
    },
    "/libraries/search": {
      "get": {
        "tags": [
          "Libraries"
        ],
        "summary": "Search libraries, categories and items",
        "description": "The native-client mirror of the web search on the **All Libraries** page\n(`Apps::LibrariesController#show` with `?q=`), shaped to the mobile\nprototype's inline results. Both surfaces resolve through the same\n`Libraries::SearchQuery`, so the API and the web page cannot drift on\nwhat matches, what ranks first, or whose libraries are searched.\n\n### What is searched\n\nThree record types, each on its own fields — echoed back in\n`searched_fields` so a client need not hard-code them:\n\n| Type | Fields |\n|------|--------|\n| Libraries  | `name`, `description` |\n| Categories | `name` (the table has no description) |\n| Items      | `title`, `description` |\n\nAn item is **not** matched by its library's or its category's name. A\nquery like `company` therefore returns the library *Company Policies*\nplus the items whose own title or description says \"company\" — not all\n28 items that happen to live in it, which would be rows with nothing in\nthem to show for the match.\n\n### Ranking\n\nEvery row carries the `search_score` it was ordered by:\n\n* library — 3 per `name` hit + 1 per `description` hit, **+ 6**\n* category — 3 per `name` hit, **+ 3**\n* item — 2 per `title` hit + 1 per `description` hit\n\nThe two bumps put places above items and libraries above categories,\nwhich is the order the mockups present them in. **Multi-term is OR**:\n`safety harness` matches rows containing either word, ranked by how many\nthey match.\n\n### What the caller sees\n\nResults are drawn from the libraries this caller may see, and\n**DISABLED LIBRARIES ARE EXCLUDED FOR EVERYONE** — including admins.\nA disabled library is hidden from everyone but an admin and excluded\nfrom search; the All Libraries index is where an admin finds it (badged,\nwith the Enable control). Everyone else sees `visibility: all_users`\nlibraries plus any whose audience rules match them.\n\n### Pagination\n\n**Only `items` paginates.** Libraries and categories are bounded by the\ntenant's own structure (tens of rows) and every mockup shows all of them\nabove the items, so `meta` describes the ITEM page while `counts`\nreports all three totals and `counts_total` their sum.\n\n**And because they do not paginate, `libraries` and `categories` are\nsent on PAGE 1 ONLY.** From `page=2` on, both are `[]` — not because\nnothing matched, but because you already have them. Re-sending them per\nitem page was pure repetition (measured 2026-09-05: `?q=a&per_page=1`\nreturned byte-identical `libraries` and `categories` blocks on pages\n1, 2 and 3, ~98% of each page's payload), and a client that APPENDS the\ntwo arrays as it pages — the natural shape for the infinite scroll this\n`meta` exists to drive — rendered every library once per page.\n\n**How to tell \"not resent\" from \"genuinely zero\":** read `counts`, which\nis computed off the match relations rather than off these arrays and so\ncarries all three totals on EVERY page. `libraries: []` with\n`counts.libraries > 0` means \"already sent on page 1\"; with\n`counts.libraries == 0` it means nothing matched. Never infer either\ntotal from an array's length.\n\n### Truncation is stated, never silent\n\nThe two unpaginated lists are capped at **100 rows each**\n(`Libraries::SearchQuery::MAX_PLACE_RESULTS`) so a one-character `q`\ncannot be a single-request dump of every library and category name in\nthe tenant. `libraries_truncated` and `categories_truncated` say when a\ncap actually bit, so a client is never handed a partial list it thinks\nis complete — the same contract, and the same key names, as the sibling\n`GET /libraries/{id}` (`categories_truncated`, `items_truncated`).\n\nBoth flags are derived from the CEILING, not from the array length:\n`counts.<type> > 100`. That is why they stay honest on page 2, where the\narrays are empty by design and a length comparison would claim\ntruncation for every two-library search.\n\n### Not offered\n\nThe mobile facet sheet (library / item type / updated / review) is not\nimplemented: the web search this mirrors has no facets, and two of the\nfour have nothing behind them here — `review` is content governance (a\ndifferent model with its own surface) and `updated` is a bucket the\nprototype derives from a hand-written string. A filter that silently\nmatched nothing would be worse than none.\n\nItem rows DO carry the caller's own bookmark state — `bookmarked` and\nthe `bookmark` sub-object — resolved in one query for the page, never\none per row. (Until 2026-09-05 they did not: the serializer's `bookmark`\nargument defaulted to nil, so every search row reported\n`bookmarked: false` while `GET /libraries/bookmarks` and\n`GET /libraries/{id}` said true for the same item in the same minute,\nand a client rendering its kebab from this flag offered \"Save\" for an\nitem already saved.)\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "The search string. Whitespace-separated terms are OR'd and scored by how many match; terms beyond the tenth are dropped (the response's `terms` reports what was actually used). `%`, `_` and `\\` are searched literally, not as wildcards. Blank or whitespace-only is a 400 — a client asking for nothing is a client with a bug, and an empty 200 would hide it.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "example": "safety harness"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "* `relevance` — by `search_score` descending, then name/title. The\n  default, and the only order the mockups show.\n* `recent` — newest `updated_at` first, within each record type.\n\n`updated` is accepted as an alias of `recent` (the mobile\nprototype's spelling); the response echoes the **canonical** key in\n`sort`. An unknown value falls back to `relevance`.\n",
            "schema": {
              "type": "string",
              "enum": [
                "relevance",
                "recent"
              ],
              "default": "relevance"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based, and applies to `items` only. A missing, zero, negative, non-numeric or collection-shaped value is 1. A page past the end is an empty `items` array with a 200, not an error.\nANY page but the first also answers `libraries: []` and `categories: []` — those two lists do not paginate and are therefore sent once, on page 1 (see Pagination above). `counts` is unaffected and reports all three totals on every page, which is how a client tells \"not resent\" from \"nothing matched\".",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Items per page, capped at 50. A value that does not parse as a positive integer (`0`, `-3`, `abc`) is treated as ABSENT and falls back to 20 — deliberately NOT floored to 1.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Search results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "query",
                    "terms",
                    "sort",
                    "available_sorts",
                    "searched_fields",
                    "counts",
                    "counts_total",
                    "libraries",
                    "categories",
                    "libraries_truncated",
                    "categories_truncated",
                    "items",
                    "meta"
                  ],
                  "properties": {
                    "query": {
                      "type": "string",
                      "description": "The query as searched — the request's `q`, trimmed.",
                      "example": "safety harness"
                    },
                    "terms": {
                      "type": "array",
                      "description": "The terms actually used: whitespace-split, de-duplicated case-insensitively, capped at ten. A client highlighting matches should use THESE, not its own split of `query`.",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "safety",
                        "harness"
                      ]
                    },
                    "sort": {
                      "type": "string",
                      "enum": [
                        "relevance",
                        "recent"
                      ],
                      "description": "The order actually applied, always as the CANONICAL key (a `?sort=updated` request answers `recent`).",
                      "example": "relevance"
                    },
                    "available_sorts": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "relevance",
                        "recent"
                      ]
                    },
                    "searched_fields": {
                      "type": "array",
                      "description": "The fields this search covered, `group.column`. Read it rather than hard-coding the list — it comes from the same constants the SQL is built from.",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "libraries.name",
                        "libraries.description",
                        "categories.name",
                        "items.title",
                        "items.description"
                      ]
                    },
                    "counts": {
                      "$ref": "#/components/schemas/LibrarySearchCounts"
                    },
                    "counts_total": {
                      "type": "integer",
                      "description": "Every match across all three types — the sum of `counts`, and the number the web results head with (\"18 results\"). NOT the item page's total, which is `meta.total_count`.\nNAMED `counts_total`, NOT `total_count`, since 2026-09-05. Both names lived in this one body meaning different numbers: measured live, `?q=brand` answered root 3 / `meta.total_count` 1, and `?q=a` answered root 227 / meta 205 — so a client reading \"total_count\" got a different answer depending on how deep it looked, and a generic paginator keyed on the root value asked for a 12th page of an 11-page list. A client written against the old name must read `counts_total` (or sum `counts` itself).",
                      "example": 18
                    },
                    "libraries": {
                      "type": "array",
                      "description": "Matching libraries, ranked — the same card the index returns, plus `search_score`.\n**PAGE 1 ONLY.** This list does not paginate, so it is sent once and every later page answers `[]`. An empty array is NOT a statement about matches: read `counts.libraries`, which is present on every page, to tell \"already sent\" from \"nothing matched\". Capped at 100 rows, with `libraries_truncated` saying when the cap bit.",
                      "items": {
                        "allOf": [
                          {
                            "type": "object",
                            "description": "One row of the All Libraries index. Every field the mockup's card renders, with the same fallbacks the web card applies — so the two surfaces cannot drift on an icon, a colour or a type label.",
                            "required": [
                              "id",
                              "title",
                              "description",
                              "image",
                              "banner_fill",
                              "icon",
                              "color",
                              "library_type",
                              "library_type_label",
                              "categories_count",
                              "items_count",
                              "updated_at",
                              "enabled",
                              "can_disable",
                              "link"
                            ],
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 42
                              },
                              "title": {
                                "type": "string",
                                "description": "The library's name.",
                                "example": "Company Policies"
                              },
                              "description": {
                                "type": "string",
                                "nullable": true,
                                "description": "The card's blurb. `null` (never `\"\"`) when the library has none.",
                                "example": "Every published company policy, grouped by the team that owns it."
                              },
                              "image": {
                                "type": "string",
                                "nullable": true,
                                "description": "ABSOLUTE URL of the library's banner image, or `null` when it has none — in which case paint `banner_fill`. Absolute because a native client cannot resolve a host-relative path.",
                                "example": "https://acme.workforce.mangoapps.com/rails/active_storage/blobs/redirect/eyJf.../banner.png"
                              },
                              "banner_fill": {
                                "type": "string",
                                "description": "The CSS gradient the web card paints behind a library with no banner image. Sent whole rather than derived client-side: the light stop is a hand-picked per-preset value, so `color` alone is not enough to reproduce it.",
                                "example": "linear-gradient(110deg,#3f5372,#7c8fb0)"
                              },
                              "icon": {
                                "type": "string",
                                "description": "Font Awesome class for the mark on the banner. Falls back to `fas fa-book-open` — the same default the web card uses — so this is never blank.",
                                "example": "fas fa-file-shield"
                              },
                              "color": {
                                "type": "string",
                                "description": "The library's hex colour, driving both the icon tint and the banner fill. Falls back to the app default (`#2f64b1`), so this is never blank and never a non-hex value.",
                                "example": "#3f5372"
                              },
                              "library_type": {
                                "type": "string",
                                "enum": [
                                  "mixed_content",
                                  "images_and_videos"
                                ],
                                "description": "The library kind, for client logic.",
                                "example": "mixed_content"
                              },
                              "library_type_label": {
                                "type": "string",
                                "description": "The display wording the banner prints. Provided because `titleize` gets it wrong — `images_and_videos` reads \"Images & Videos\", not \"Images And Videos\".",
                                "example": "Mixed Content"
                              },
                              "categories_count": {
                                "type": "integer",
                                "description": "Number of categories in this library (the folder icon on the card). Computed by a scalar subquery, so it costs nothing per row.",
                                "example": 6
                              },
                              "items_count": {
                                "type": "integer",
                                "description": "Number of items across all of this library's categories (the document icon on the card). Also a scalar subquery.",
                                "example": 28
                              },
                              "updated_at": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true,
                                "description": "ISO8601. The same timestamp `sort=recent` orders on and the card renders as \"Updated N ago\".",
                                "example": "2026-08-29T06:32:39Z"
                              },
                              "enabled": {
                                "type": "boolean",
                                "description": "`false` for a disabled library — draw the \"Disabled\" badge and grey the row. Only a Libraries admin ever receives a `false` here.",
                                "example": true
                              },
                              "can_disable": {
                                "type": "boolean",
                                "description": "Whether this caller may Disable / Enable this library. App-wide, so identical on every row of a response and equal to the top-level `can_disable`; carried per-row so a client rendering one card in isolation needs no extra context.",
                                "example": true
                              },
                              "link": {
                                "type": "string",
                                "nullable": true,
                                "description": "ABSOLUTE web URL of the library — for the row menu's Copy link, and for opening the library in a webview.",
                                "example": "https://acme.workforce.mangoapps.com/apps/libraries/spaces/42"
                              }
                            }
                          },
                          {
                            "type": "object",
                            "properties": {
                              "search_score": {
                                "type": "integer",
                                "description": "The score this row was ranked by.",
                                "example": 10
                              }
                            }
                          }
                        ]
                      }
                    },
                    "categories": {
                      "type": "array",
                      "description": "Matching categories, ranked. **PAGE 1 ONLY**, on the same terms as `libraries` above: `[]` from page 2 on, with `counts.categories` as the total that stays truthful on every page. Capped at 100 rows, with `categories_truncated` saying when the cap bit.",
                      "items": {
                        "$ref": "#/components/schemas/LibrarySearchCategory"
                      }
                    },
                    "libraries_truncated": {
                      "type": "boolean",
                      "description": "True when more than 100 libraries matched (`Libraries::SearchQuery::MAX_PLACE_RESULTS`) and the `libraries` array therefore holds only the first 100. Present on EVERY page, and derived from `counts.libraries > 100` rather than from the array's length — which is what keeps it honest on page 2, where the array is empty by design.\nRead it. It exists so a client is never silently handed a partial list; nothing else in the response says the list was clipped.",
                      "example": false
                    },
                    "categories_truncated": {
                      "type": "boolean",
                      "description": "The same flag for `categories`, against the same 100-row ceiling and the same `counts.categories > 100` derivation.",
                      "example": false
                    },
                    "items": {
                      "type": "array",
                      "description": "One page of matching items, ranked. The same card `GET /libraries/bookmarks` returns (same serializer), plus `search_score`. `bookmarked` and `bookmark` carry the CALLER'S OWN saved state here, resolved in one query for the page — they are not the constant false/null this endpoint returned before 2026-09-05.",
                      "items": {
                        "allOf": [
                          {
                            "type": "object",
                            "description": "One saved library item — everything a row, its 3-dot sheet and its details panel need, from one call.",
                            "required": [
                              "id",
                              "title",
                              "item_type",
                              "item_type_label",
                              "icon_type",
                              "icon",
                              "breadcrumb",
                              "status",
                              "open_mode",
                              "bookmarked"
                            ],
                            "properties": {
                              "id": {
                                "type": "integer",
                                "description": "The LIBRARY ITEM id — what the bookmark write endpoints take.",
                                "example": 412
                              },
                              "title": {
                                "type": "string",
                                "example": "Remote Work Policy"
                              },
                              "description": {
                                "type": "string",
                                "nullable": true
                              },
                              "item_type": {
                                "type": "string",
                                "description": "The stored item kind. Branch logic on this, not on `item_type_label`.",
                                "enum": [
                                  "simple_link",
                                  "file",
                                  "form",
                                  "survey",
                                  "wiki",
                                  "post",
                                  "image",
                                  "video"
                                ],
                                "example": "file"
                              },
                              "item_type_label": {
                                "type": "string",
                                "description": "Display label, EXTENSION-specific where the app can tell (\"PDF\", \"Word\", \"Excel\") and type-level otherwise (\"Link\", \"Form\", \"Wiki\"). The same label the web row prints.\nNAMED `item_type_label`, NOT `type_label`, since 2026-09-05: the column is `item_type`, every other label in this repo is `<prefix>_type_label` (including `library_type_label` on the library card and `item_type_label` on `GET /libraries/{id}`), and the bare spelling meant one concept arrived under two names depending on which Libraries endpoint a client called.",
                                "example": "PDF"
                              },
                              "icon_type": {
                                "type": "string",
                                "description": "WHAT KIND OF STRING `icon` IS — the discriminator, because the glyph cannot carry its own domain. `default` and `custom` mean `icon` is a Font Awesome CLASS; `emoji` means `icon` is a raw emoji GRAPHEME.\nRead this before you render. A client that maps `icon` to an icon font unconditionally draws tofu for every emoji row, and one that prints it as text shows \"fas fa-file-pdf\" for the rest. No tenant holds an emoji row today, but Load Sample Data seeds three, so this is one button press from live rather than theoretical.\nNullable, because the column is: `library_items.icon_type` has no NOT NULL and the model's enum takes `allow_nil`, so an explicitly cleared row is a legal state. Treat null as `default`.",
                                "enum": [
                                  "default",
                                  "emoji",
                                  "custom",
                                  null
                                ],
                                "nullable": true,
                                "example": "default"
                              },
                              "icon": {
                                "type": "string",
                                "description": "The glyph — a Font Awesome class or an emoji grapheme, per `icon_type` above. Extension-specific where known; overridden by the item's own icon when an admin set one.",
                                "example": "fas fa-file-pdf"
                              },
                              "icon_color": {
                                "type": "string",
                                "description": "Hex glyph colour.",
                                "example": "#c0392b"
                              },
                              "icon_background": {
                                "type": "string",
                                "description": "Fill for the plate behind the glyph. Hex, or an `rgba()` string when derived from an admin-chosen item colour.",
                                "example": "#fbe7e5"
                              },
                              "format": {
                                "type": "string",
                                "nullable": true,
                                "description": "Lowercase file extension without the dot, or null when the item has nothing to infer one from (a form, a wiki page, a bare link).",
                                "example": "pdf"
                              },
                              "library": {
                                "type": "object",
                                "nullable": true,
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 7
                                  },
                                  "name": {
                                    "type": "string",
                                    "example": "Company Policies"
                                  },
                                  "icon": {
                                    "type": "string",
                                    "example": "fas fa-shield-halved"
                                  },
                                  "color": {
                                    "type": "string",
                                    "example": "#3478f6"
                                  },
                                  "enabled": {
                                    "type": "boolean",
                                    "description": "False only for a Libraries administrator looking at a disabled library's saved item — grey the row and badge it \"Disabled\".",
                                    "example": true
                                  }
                                }
                              },
                              "category": {
                                "type": "object",
                                "nullable": true,
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 21
                                  },
                                  "name": {
                                    "type": "string",
                                    "example": "Leave & Time Off"
                                  },
                                  "position": {
                                    "type": "integer",
                                    "example": 0
                                  }
                                }
                              },
                              "breadcrumb": {
                                "type": "string",
                                "nullable": true,
                                "description": "`Library › Category`, pre-joined with the separator the design uses.",
                                "example": "Company Policies › Leave & Time Off"
                              },
                              "status": {
                                "type": "string",
                                "description": "`available` — opens normally. `inactive` — the linked record was unpublished/deactivated after it was linked, OR this reader may not open it; show the details panel, not a link. `unavailable` — the linked record is gone; a library manager must relink or remove it.",
                                "enum": [
                                  "available",
                                  "inactive",
                                  "unavailable"
                                ],
                                "example": "available"
                              },
                              "status_label": {
                                "type": "string",
                                "nullable": true,
                                "description": "The badge text the web renders beside the row (\"Inactive\", \"Source unavailable\"), or null when the item is healthy.",
                                "example": null
                              },
                              "url": {
                                "type": "string",
                                "nullable": true,
                                "description": "Absolute destination — where a tap goes. NULL whenever `open_mode` is `unavailable`, including for an item this particular reader may not open even though a link exists for someone else.\nALSO NULL when `open_mode` is `form`: a form is opened BY ID, from `form_id`. Branch on `open_mode`, never on which of the two is set."
                              },
                              "form_id": {
                                "type": "integer",
                                "nullable": true,
                                "description": "The id of the linked form — the locator that replaced a form item's web URL, for `open_mode: form` only. Open your own form screen with it. Null for every other type, and null for a form this reader may not open (same rule `url` follows: `status` / `status_label` say why).",
                                "example": null
                              },
                              "copy_link_url": {
                                "type": "string",
                                "nullable": true,
                                "description": "What \"Copy link\" puts on the clipboard: the absolute destination. The same value as `url` for every type EXCEPT a form, where it is the card's only URL — the clipboard's job is to produce something a person can paste, and a form id pastes into nothing. Null whenever `open_mode` is `unavailable`."
                              },
                              "link_url": {
                                "type": "string",
                                "nullable": true,
                                "description": "The source URL an admin stored on the item, which the details panel prints on its own row. Distinct from `url`, which is where a tap goes."
                              },
                              "opens_in": {
                                "type": "string",
                                "nullable": true,
                                "description": "The item's configured link target — what the web anchor's `target` reads. `GET /libraries/{id}` carries the same column as `open.target`.\nNULLABLE, and a typed client must model it as optional. `library_items.link_target` is `t.string default: \"new_tab\"` with no NOT NULL, the model's enum takes `allow_nil`, and the item form deliberately submits an empty value — which casts back to nil — for every item type whose \"Open link in\" panel is never shown, so an unanswered question stays unanswered rather than being answered \"New tab\" on the admin's behalf. No row holds null today; the first one would crash a client with a non-optional String here.",
                                "enum": [
                                  "new_tab",
                                  "current_tab",
                                  null
                                ],
                                "example": "new_tab"
                              },
                              "open_mode": {
                                "type": "string",
                                "description": "How to open it, and which of `url` / `form_id` carries the locator. `form` — a form this app hosts; route to your own form screen using `form_id` (`url` is null). `external` — an off-platform http(s) URL; hand it to the system browser. `preview` — a file this app serves; open the file preview or the media viewer. `in_app` — a path inside the platform (a wiki page, a survey, a document record). `unavailable` — nothing to open; show the details panel and `status_label` says why.",
                                "enum": [
                                  "form",
                                  "external",
                                  "preview",
                                  "in_app",
                                  "unavailable"
                                ],
                                "example": "preview"
                              },
                              "download_url": {
                                "type": "string",
                                "nullable": true,
                                "description": "Absolute URL for the same blob served as an attachment, or null when the item has no file to save (a link, a form, a wiki page, or a document record that stores no blob of its own). Omit the Download row from the sheet when null rather than showing it inert."
                              },
                              "media": {
                                "type": "object",
                                "nullable": true,
                                "description": "The item's own uploaded file. Null when it has none.",
                                "properties": {
                                  "filename": {
                                    "type": "string",
                                    "example": "remote-work.pdf"
                                  },
                                  "content_type": {
                                    "type": "string",
                                    "example": "application/pdf"
                                  },
                                  "byte_size": {
                                    "type": "integer",
                                    "example": 1153434
                                  },
                                  "size_label": {
                                    "type": "string",
                                    "description": "Pre-formatted, matching what the details panel prints.",
                                    "example": "1.1 MB"
                                  },
                                  "width": {
                                    "type": "integer",
                                    "nullable": true,
                                    "description": "Pixel width, ALWAYS an integer, and null — never zero — for a blob Active Storage has not analyzed yet.\nInteger is a coercion, not a passthrough: the two analyzers disagree on type. The image analyzer writes vips' Integer while the VIDEO analyzer writes `Float(video_stream[\"width\"])` by design, and this key used to ship whichever the row happened to hold — 2,000 image blobs as Integer, 149 video blobs as Float, under one key. Since 2026-09-05 both are coerced, which is safe because a pixel count is a whole number in either analyzer.",
                                    "example": 2400
                                  },
                                  "height": {
                                    "type": "integer",
                                    "nullable": true,
                                    "description": "Pixel height. Integer on the same terms as `width`.",
                                    "example": 1600
                                  },
                                  "dimensions": {
                                    "type": "string",
                                    "nullable": true,
                                    "description": "`width × height`, or null when either is unknown. Integral on both sides — it read \"1920.0 × 1080.0\" for every video until the coercion above landed.",
                                    "example": "2400 × 1600"
                                  },
                                  "duration_seconds": {
                                    "type": "number",
                                    "nullable": true,
                                    "description": "Video length. Null until background analysis has run on a freshly uploaded file — that is a real \"not yet known\", not a zero.",
                                    "example": 92.4
                                  },
                                  "duration_label": {
                                    "type": "string",
                                    "nullable": true,
                                    "description": "`m:ss` or `h:mm:ss`.",
                                    "example": "1:32"
                                  },
                                  "url": {
                                    "type": "string",
                                    "description": "Absolute URL for the blob served INLINE (preview / viewer)."
                                  }
                                }
                              },
                              "created_by": {
                                "type": "object",
                                "nullable": true,
                                "description": "Who added the item — the details panel's \"Added by\" byline. NAMED `created_by`, NOT `added_by`, since 2026-09-05: those are the names the columns actually have, and the names every other payload in this namespace uses (`GET /libraries/{id}` for both the item and the library). `added_by` / `added_at` were the only two occurrences of that spelling in the whole `/api/v1` surface — 2 sites against 254 — so a client decoding \"who added this, and when\" needed two field names depending on the endpoint.",
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 88
                                  },
                                  "name": {
                                    "type": "string",
                                    "example": "Neha Kulkarni"
                                  }
                                }
                              },
                              "created_at": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true,
                                "description": "When the ITEM was added to the library. Not to be confused with `bookmark.created_at`, which is when THIS CALLER saved it — the key this list is ordered by."
                              },
                              "updated_by": {
                                "type": "object",
                                "nullable": true,
                                "description": "Null until somebody edits the item.",
                                "properties": {
                                  "id": {
                                    "type": "integer"
                                  },
                                  "name": {
                                    "type": "string"
                                  }
                                }
                              },
                              "updated_at": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true
                              },
                              "can_manage": {
                                "type": "boolean",
                                "description": "May this caller edit / move / delete items in this library. Gates the 3-dot sheet's Edit, Move and Delete rows; it follows the library's management level, not the caller's role alone.",
                                "example": false
                              },
                              "bookmarked": {
                                "type": "boolean",
                                "description": "Always true on this endpoint. Present so one card type serves every list.",
                                "example": true
                              },
                              "bookmark": {
                                "type": "object",
                                "nullable": true,
                                "description": "The caller's own bookmark row.",
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "description": "The BOOKMARK id (not the item id). Useful for local ordering; the bookmark write endpoints address the ITEM, not this.",
                                    "example": 9021
                                  },
                                  "note": {
                                    "type": "string",
                                    "nullable": true,
                                    "description": "The caller's own note, authored on the web /bookmarks page.",
                                    "example": "Read before the audit"
                                  },
                                  "created_at": {
                                    "type": "string",
                                    "format": "date-time",
                                    "description": "When it was saved — the sort key of this list."
                                  }
                                }
                              }
                            }
                          },
                          {
                            "type": "object",
                            "properties": {
                              "search_score": {
                                "type": "integer",
                                "description": "The score this row was ranked by.",
                                "example": 4
                              }
                            }
                          }
                        ]
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/LibrarySearchItemPagination"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "Piggybacked on every response in this API for native badge management. Unrelated to Libraries.",
                      "example": 3
                    },
                    "_meta": {
                      "type": "object",
                      "description": "Standard response metadata",
                      "additionalProperties": true,
                      "properties": {
                        "request_id": {
                          "type": "string",
                          "description": "Unique request identifier"
                        },
                        "generated_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "Timestamp when the response was generated"
                        },
                        "execution_time_ms": {
                          "type": "number",
                          "description": "Server processing time in milliseconds"
                        },
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items"
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages"
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number"
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Number of items per page"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`q` was missing, blank or whitespace-only (`invalid_request`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "The token lacks `read:libraries` (`insufficient_permissions`), or the Libraries app is not accessible to this caller — not enabled for the tenant, or the caller is outside the app's audience (`access_denied`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/libraries/{id}/disable": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The LIBRARY id (not a category, and not an item).",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Libraries"
        ],
        "summary": "Disable a library",
        "description": "Takes the library **out of circulation** — the native mirror of the web\n**Disable** control (the All Libraries row menu and the library header,\nboth of which reach\n`Apps::Libraries::SpacesController#toggle_enabled`). Both surfaces perform\nthe transition through the shared `Libraries::EnablementService`, so\nneither can drift on what disabling means, who may do it, or what the\nconfirmation says.\n\nA disabled library **drops off every browse, search and mobile surface**.\nIt stays visible — badged `enabled: false` — only to callers who may\nadminister Libraries, because the index is the surface that offers the way\nback. Its categories, items and audience rules are **untouched**: this is\na reversible hide, not a delete.\n\n### Idempotent, deliberately NOT a toggle\n\nThis is the one place the endpoint diverges from the web control it\nmirrors. A button can safely mean \"flip it\"; a network client retries, and\na second toggle would silently **re-enable** a library the caller had just\ntaken down.\n\nSo disabling an already-disabled library is a no-op that still answers\n`200`: it writes nothing, leaves `updated_at` untouched, and reports\n`changed: false`. Ten retries are indistinguishable from one call. Read\n`changed` to know whether this call was the one that moved the flag.\n\nThe way back is the sibling verb, `POST /libraries/{id}/enable`. The web\nrenders both as ONE control whose label flips; this API splits it into\ntwo verbs precisely so a retry cannot undo itself.\n\n### Authorization — three gates\n\n1. **Token scope `write:libraries`.** Unlike the read surface in this\n   file, the write is scope-gated. The scope IS grantable: it reaches\n   `ApiToken::AVAILABLE_SCOPES` through `Mcp::ScopeRegistry`, which mints\n   `read:` / `write:` / `destructive:` for every `Agents::ToolRegistry`\n   domain, so an admin-issued token can hold it and the mobile login paths\n   already mint it. `has_scope?` fails **closed** on a scopeless token, and\n   the blanket `admin` scope satisfies it. A session-authenticated caller\n   (native WebView, internal call) carries no token and passes, exactly as\n   `require_scope` defines it.\n2. **Libraries app access** — enablement (explicit row OR pricing tier OR\n   active subscription) AND the app's per-group visibility. Otherwise\n   `403 access_denied`.\n3. **Libraries administrator** — `Libraries::Access#admin?`, precisely\n   what the web's `authorize_admin_access` asks, and precisely what the\n   index's `can_disable` flag reports. Business admins, super admins and\n   **per-app Libraries admins** all qualify; a manager with no Libraries\n   grant does not. Otherwise `403 forbidden`.\n\nGate 3 is **app-wide, not per-library**, and is answered BEFORE the\nlibrary is resolved. So an unauthorized caller gets `403` for a real id\nand for a nonexistent one alike, and cannot enumerate library ids by\nreading `403`-vs-`404`. A library belonging to another tenant is `404`,\nnever `403` — a `403` would confirm the id is real somewhere.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The library is disabled. Returned whether this call changed it or it was already off — read `changed` to tell those apart.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "library",
                    "changed",
                    "message"
                  ],
                  "properties": {
                    "library": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LibraryCard"
                        }
                      ],
                      "description": "The library AFTER the call, in the same card shape `GET /libraries/list` emits — counts included — so a client redraws the row from this one response with no follow-up. `enabled` is always `false` here."
                    },
                    "changed": {
                      "type": "boolean",
                      "description": "`true` when this call moved the flag; `false` when the library was already disabled and nothing was written.",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "description": "Confirmation copy, byte-identical to the web flash — it describes what is TRUE now, so it reads the same on a retry.",
                      "enum": [
                        "Library disabled successfully."
                      ],
                      "example": "Library disabled successfully."
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "Piggybacked on every response in this API for native badge management. Unrelated to Libraries.",
                      "example": 3
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API token (`authentication_required`)."
          },
          "403": {
            "description": "The token lacks `write:libraries` (`insufficient_permissions`); the Libraries app is not enabled for the tenant or the caller is outside its audience (`access_denied`); or the caller is not a Libraries administrator (`forbidden`). Nothing is written in any of the three."
          },
          "404": {
            "description": "No library with that id exists in the caller's tenant (`not_found`). Another tenant's library answers this too — never `403`."
          },
          "422": {
            "description": "The library could not be saved (`invalid`). The `message` names the validation that refused it."
          }
        }
      }
    },
    "/libraries/{id}/enable": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The LIBRARY id (not a category, and not an item).",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Libraries"
        ],
        "summary": "Enable a library",
        "description": "Puts a disabled library **back into circulation** — the way back from\n`POST /libraries/{id}/disable`, and the native mirror of the web\n**Enable** control.\n\nOn the web, Enable and Disable are the SAME control: the All Libraries\nrow menu and the library header render one entry whose label flips with\nthe current state, and both reach\n`Apps::Libraries::SpacesController#toggle_enabled`. Every surface performs\nthe transition through the shared `Libraries::EnablementService`, so none\ncan drift on what enabling means, who may do it, or what the confirmation\nsays.\n\nEnabling restores the library to **every browse, search and mobile\nsurface**. Nothing else about it changes — disabling never touched its\ncategories, items or audience rules, so there is nothing to restore\nbeyond the flag.\n\n### Idempotent, deliberately NOT a toggle\n\nA button can safely mean \"flip it\" because a person sees the result; a\nnetwork client retries, and a retried toggle undoes itself. So this API\nsplits the web's single control into two verbs that each name the state\nthey move **to**.\n\nEnabling an already-enabled library is a no-op that still answers `200`:\nit writes nothing, leaves `updated_at` untouched, and reports\n`changed: false`. Ten retries are indistinguishable from one call. Read\n`changed` to know whether this call was the one that moved the flag.\n\n### Authorization — three gates\n\nIdentical to `POST /libraries/{id}/disable`, and deliberately so: ONE\ncapability governs both directions, which means a caller who may take a\nlibrary down may always put it back.\n\n1. **Token scope `write:libraries`.** The scope IS grantable — it reaches\n   `ApiToken::AVAILABLE_SCOPES` through `Mcp::ScopeRegistry` and the\n   mobile login paths already mint it. `has_scope?` fails **closed** on a\n   scopeless token, and the blanket `admin` scope satisfies it. A\n   session-authenticated caller (native WebView, internal call) carries no\n   token and passes, exactly as `require_scope` defines it.\n2. **Libraries app access** — enablement (explicit row OR pricing tier OR\n   active subscription) AND the app's per-group visibility. Otherwise\n   `403 access_denied`.\n3. **Libraries administrator** — `Libraries::Access#admin?`, precisely\n   what the web's `authorize_admin_access` asks, and precisely what the\n   index's `can_disable` flag reports. Business admins, super admins and\n   **per-app Libraries admins** all qualify; a manager with no Libraries\n   grant does not. Otherwise `403 forbidden`.\n\nGate 3 is **app-wide, not per-library**, and is answered BEFORE the\nlibrary is resolved. So an unauthorized caller gets `403` for a real id\nand for a nonexistent one alike, and cannot enumerate library ids by\nreading `403`-vs-`404`. A library belonging to another tenant is `404`,\nnever `403` — a `403` would confirm the id is real somewhere.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The library is enabled. Returned whether this call changed it or it was already on — read `changed` to tell those apart.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "library",
                    "changed",
                    "message"
                  ],
                  "properties": {
                    "library": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LibraryCard"
                        }
                      ],
                      "description": "The library AFTER the call, in the same card shape `GET /libraries/list` emits — counts included — so a client redraws the row from this one response with no follow-up. `enabled` is always `true` here."
                    },
                    "changed": {
                      "type": "boolean",
                      "description": "`true` when this call moved the flag; `false` when the library was already enabled and nothing was written.",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "description": "Confirmation copy, byte-identical to the web flash — it describes what is TRUE now, so it reads the same on a retry.",
                      "enum": [
                        "Library enabled successfully."
                      ],
                      "example": "Library enabled successfully."
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "Piggybacked on every response in this API for native badge management. Unrelated to Libraries.",
                      "example": 3
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API token (`authentication_required`)."
          },
          "403": {
            "description": "The token lacks `write:libraries` (`insufficient_permissions`); the Libraries app is not enabled for the tenant or the caller is outside its audience (`access_denied`); or the caller is not a Libraries administrator (`forbidden`). Nothing is written in any of the three."
          },
          "404": {
            "description": "No library with that id exists in the caller's tenant (`not_found`). Another tenant's library answers this too — never `403`."
          },
          "422": {
            "description": "The library could not be saved (`invalid`). The `message` names the validation that refused it."
          }
        }
      }
    },
    "/libraries/items/{id}/bookmark": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The library ITEM id (not the library, and not the category).",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Libraries"
        ],
        "summary": "Bookmark a library item for the current user",
        "description": "Saves the item for the **calling user**, writing the platform-wide\n`Platform::Bookmark` shared with the web item kebab's **Bookmark this**\nentry, the `/bookmarks` page and My Stuff. An item saved on the phone is\nsaved on the web, and vice versa — there is no app-local \"library item\npin\".\n\nBookmarks in Libraries are **items-only**: never a library, never a\ncategory. That is why the path carries an explicit `items/` segment\nrather than hanging off a library id.\n\n**Idempotent** — bookmarking an already-bookmarked item is a no-op that\nstill returns `bookmarked: true` and never creates a second row. This is\nthe deliberate divergence from the web control, which is a single\n*toggle* because one button serves both directions: a retried `POST`\nhere must never silently un-bookmark. Read the saved set back with\n`GET /libraries/bookmarks`.\n\n* **Authorization — anyone who can OPEN the item may bookmark it.**\n  Requires the `write:libraries` scope (the floor every write in this\n  namespace applies) and Libraries-app access. The item is then resolved\n  inside the caller's visible scope: visibility is inherited from the\n  parent library's audience, so a restricted library, a disabled one, a\n  cross-tenant id and a missing id all return `404` alike — none of them\n  reveals that the item exists.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Item bookmarked (or already bookmarked).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LibraryItemBookmarkState"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API token"
          },
          "403": {
            "description": "Missing the `write:libraries` scope (`insufficient_permissions`), or the Libraries app is not accessible to the caller (`access_denied`)."
          },
          "404": {
            "description": "No library item with that id is visible to the caller (`not_found`)."
          }
        }
      },
      "delete": {
        "tags": [
          "Libraries"
        ],
        "summary": "Remove the current user's bookmark on a library item",
        "description": "Removes the calling user's bookmark on the item. **Idempotent** —\nremoving a bookmark that isn't there is a no-op that still returns\n`bookmarked: false`.\n\nA bookmark carrying a note is soft-deleted (recoverable from the\nplatform Trash); a bare one is removed outright. That asymmetry is the\nweb toggle's, and it exists so routine un-saves don't bury the things\nTrash is for.\n\n* **Authorization is deliberately ASYMMETRIC with the POST verb.** An\n  **already-saved** bookmark can always be cleared — even once the item\n  has gone out of reach, because its library was disabled or its\n  audience narrowed. `GET /libraries/bookmarks` drops such rows from the\n  list (listing them would leak the title of content the caller cannot\n  open), so this verb is the only way they can be cleared and must not\n  refuse them. Removing something that is **neither bookmarked nor\n  visible** still returns `404`, so the verb never becomes a way to\n  probe which item ids exist. Scope and app-access gates are unchanged.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Bookmark removed (or there was none).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LibraryItemBookmarkState"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API token"
          },
          "403": {
            "description": "Missing the `write:libraries` scope (`insufficient_permissions`), or the Libraries app is not accessible to the caller (`access_denied`)."
          },
          "404": {
            "description": "The caller has no bookmark on that item AND no library item with that id is visible to them (`not_found`)."
          }
        }
      }
    },
    "/libraries/bookmarks": {
      "get": {
        "tags": [
          "Libraries"
        ],
        "summary": "The caller's saved (bookmarked) library items",
        "description": "The library items the calling user has bookmarked — most recently saved\nfirst — plus the per-library groups needed to render them grouped.\n\nReturns `200` with an empty `items` array (never an error) when the\ncaller has saved nothing.\n\nSee the file header for ordering, the exclusion rules, the authorization\nmodel, and the fields that are deliberately absent.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based page number. A missing, zero, negative or non-numeric value is treated as page 1. A client that array-wraps its query params (`?page[]=2`) is honoured as the scalar rather than refused.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            },
            "example": 1
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Rows per page, clamped to 1..50. A value that does not parse as a positive integer is treated as ABSENT and falls back to 20 — it is NOT clamped up to 1.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            },
            "example": 20
          }
        ],
        "responses": {
          "200": {
            "description": "Saved items retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "libraries",
                    "groups_total",
                    "meta"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "description": "This page of saved items, most recently saved first.",
                      "items": {
                        "$ref": "#/components/schemas/LibraryBookmarkedItem"
                      }
                    },
                    "libraries": {
                      "type": "array",
                      "description": "One row per library the caller has saved something in, with the count of their saved items in it. Counted over the whole saved set (not the page), ordered by count descending then name, and containing no library the caller cannot see.\n**PAGE 1 ONLY.** This array does not paginate — it describes the whole saved set — so it is sent once and every later page answers `[]`. An empty array is therefore not a statement about how much the caller has saved: read `groups_total`, which is present on every page. See the file header for the measurement behind this.",
                      "items": {
                        "$ref": "#/components/schemas/LibraryBookmarkGroup"
                      }
                    },
                    "groups_total": {
                      "type": "integer",
                      "description": "How many library groups the caller's saved set spans — present and complete on EVERY page, including the pages where `libraries` is `[]`.\nThis is the field that makes the page-1-only contract readable: `libraries: []` with `groups_total > 0` means \"already sent on page 1\", and with `groups_total == 0` means \"nothing saved\". A client that instead took `libraries.length` as the group count would report zero groups for every page after the first.",
                      "example": 2
                    },
                    "meta": {
                      "$ref": "#/components/schemas/LibraryBookmarkPagination"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "Piggybacked on every response in this API for native badge management. Unrelated to Libraries.",
                      "example": 3
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                },
                "examples": {
                  "saved": {
                    "summary": "Two saved items across two libraries — and the two shapes of `icon`",
                    "description": "The first row is `icon_type: default`, so `icon` is a Font Awesome class. The second is `icon_type: emoji`, so `icon` is a raw emoji GRAPHEME — pass it to an icon font and the row renders tofu. Switch on `icon_type`, never on the string's shape.",
                    "value": {
                      "items": [
                        {
                          "id": 412,
                          "title": "Remote Work Policy",
                          "description": "How and when you can work remotely",
                          "item_type": "file",
                          "item_type_label": "PDF",
                          "icon_type": "default",
                          "icon": "fas fa-file-pdf",
                          "icon_color": "#c0392b",
                          "icon_background": "#fbe7e5",
                          "format": "pdf",
                          "library": {
                            "id": 7,
                            "name": "Company Policies",
                            "icon": "fas fa-shield-halved",
                            "color": "#3478f6",
                            "enabled": true
                          },
                          "category": {
                            "id": 21,
                            "name": "Leave & Time Off",
                            "position": 0
                          },
                          "breadcrumb": "Company Policies › Leave & Time Off",
                          "status": "available",
                          "status_label": null,
                          "url": "https://acme.workforce.mangoapps.com/rails/active_storage/blobs/redirect/eyJf/remote-work.pdf",
                          "form_id": null,
                          "copy_link_url": "https://acme.workforce.mangoapps.com/rails/active_storage/blobs/redirect/eyJf/remote-work.pdf",
                          "link_url": null,
                          "opens_in": "new_tab",
                          "open_mode": "preview",
                          "download_url": "https://acme.workforce.mangoapps.com/rails/active_storage/blobs/redirect/eyJf/remote-work.pdf?disposition=attachment",
                          "media": {
                            "filename": "remote-work.pdf",
                            "content_type": "application/pdf",
                            "byte_size": 1153434,
                            "size_label": "1.1 MB",
                            "width": null,
                            "height": null,
                            "dimensions": null,
                            "duration_seconds": null,
                            "duration_label": null,
                            "url": "https://acme.workforce.mangoapps.com/rails/active_storage/blobs/redirect/eyJf/remote-work.pdf"
                          },
                          "created_by": {
                            "id": 88,
                            "name": "Neha Kulkarni"
                          },
                          "created_at": "2026-03-12T09:14:00Z",
                          "updated_by": null,
                          "updated_at": "2026-08-30T11:02:00Z",
                          "can_manage": false,
                          "bookmarked": true,
                          "bookmark": {
                            "id": 9021,
                            "note": "Read before the audit",
                            "created_at": "2026-08-31T16:40:12Z"
                          }
                        },
                        {
                          "id": 512,
                          "title": "Corporate card portal",
                          "description": "Statements, limits and dispute forms",
                          "item_type": "simple_link",
                          "item_type_label": "Link",
                          "icon_type": "emoji",
                          "icon": "💳",
                          "icon_color": "#11936f",
                          "icon_background": "#d4efe7",
                          "format": null,
                          "library": {
                            "id": 9,
                            "name": "Brand Assets",
                            "icon": "fas fa-palette",
                            "color": "#8b5cf6",
                            "enabled": true
                          },
                          "category": {
                            "id": 33,
                            "name": "Finance",
                            "position": 2
                          },
                          "breadcrumb": "Brand Assets › Finance",
                          "status": "available",
                          "status_label": null,
                          "url": "https://intranet.acme.com/cards",
                          "form_id": null,
                          "copy_link_url": "https://intranet.acme.com/cards",
                          "link_url": "https://intranet.acme.com/cards",
                          "opens_in": "new_tab",
                          "open_mode": "external",
                          "download_url": null,
                          "media": null,
                          "created_by": {
                            "id": 91,
                            "name": "Rajveer Sandhu"
                          },
                          "created_at": "2026-07-27T10:00:00Z",
                          "updated_by": null,
                          "updated_at": "2026-08-18T10:00:00Z",
                          "can_manage": true,
                          "bookmarked": true,
                          "bookmark": {
                            "id": 9020,
                            "note": null,
                            "created_at": "2026-08-29T08:11:04Z"
                          }
                        }
                      ],
                      "libraries": [
                        {
                          "id": 7,
                          "title": "Company Policies",
                          "icon": "fas fa-shield-halved",
                          "color": "#3478f6",
                          "library_type": "mixed_content",
                          "enabled": true,
                          "bookmarked_count": 1
                        },
                        {
                          "id": 9,
                          "title": "Brand Assets",
                          "icon": "fas fa-palette",
                          "color": "#8b5cf6",
                          "library_type": "mixed_content",
                          "enabled": true,
                          "bookmarked_count": 1
                        }
                      ],
                      "groups_total": 2,
                      "meta": {
                        "total_count": 2,
                        "total_pages": 1,
                        "current_page": 1,
                        "per_page": 20,
                        "has_next_page": false,
                        "has_prev_page": false
                      }
                    }
                  },
                  "saved_form": {
                    "summary": "A saved FORM item — opened by id, not by URL",
                    "description": "`open_mode: form` and `url: null`; the locator is `form_id`. `copy_link_url` is still a real URL, because that is what the clipboard needs.",
                    "value": {
                      "items": [
                        {
                          "id": 618,
                          "title": "Expense Claim",
                          "description": "Submit receipts for reimbursement",
                          "item_type": "form",
                          "item_type_label": "Form",
                          "icon_type": "default",
                          "icon": "fas fa-clipboard-list",
                          "icon_color": "#9a5b06",
                          "icon_background": "#fbeecd",
                          "format": null,
                          "library": {
                            "id": 7,
                            "name": "Company Policies",
                            "icon": "fas fa-shield-halved",
                            "color": "#3478f6",
                            "enabled": true
                          },
                          "category": {
                            "id": 24,
                            "name": "Finance",
                            "position": 1
                          },
                          "breadcrumb": "Company Policies › Finance",
                          "status": "available",
                          "status_label": null,
                          "url": null,
                          "form_id": 583,
                          "copy_link_url": "https://acme.workforce.mangoapps.com/apps/forms/templates/583",
                          "link_url": null,
                          "opens_in": "new_tab",
                          "open_mode": "form",
                          "download_url": null,
                          "media": null,
                          "created_by": {
                            "id": 88,
                            "name": "Neha Kulkarni"
                          },
                          "created_at": "2026-05-04T08:30:00Z",
                          "updated_by": null,
                          "updated_at": "2026-05-04T08:30:00Z",
                          "can_manage": false,
                          "bookmarked": true,
                          "bookmark": {
                            "id": 9033,
                            "note": null,
                            "created_at": "2026-09-01T12:05:00Z"
                          }
                        }
                      ],
                      "libraries": [
                        {
                          "id": 7,
                          "title": "Company Policies",
                          "icon": "fas fa-shield-halved",
                          "color": "#3478f6",
                          "library_type": "mixed_content",
                          "enabled": true,
                          "bookmarked_count": 1
                        }
                      ],
                      "groups_total": 1,
                      "meta": {
                        "total_count": 1,
                        "total_pages": 1,
                        "current_page": 1,
                        "per_page": 20,
                        "has_next_page": false,
                        "has_prev_page": false
                      }
                    }
                  },
                  "empty": {
                    "summary": "Nothing saved yet",
                    "value": {
                      "items": [],
                      "libraries": [],
                      "groups_total": 0,
                      "meta": {
                        "total_count": 0,
                        "total_pages": 0,
                        "current_page": 1,
                        "per_page": 20,
                        "has_next_page": false,
                        "has_prev_page": false
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token"
          },
          "403": {
            "description": "`insufficient_permissions` — the token lacks `read:libraries`; or `access_denied` — the Libraries app is not accessible to this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "insufficient_permissions",
                            "access_denied"
                          ]
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "no_scope": {
                    "value": {
                      "error": {
                        "code": "insufficient_permissions",
                        "message": "Required scope: read:libraries"
                      }
                    }
                  },
                  "no_app": {
                    "value": {
                      "error": {
                        "code": "access_denied",
                        "message": "You do not have access to the Libraries app"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/libraries/{id}": {
      "get": {
        "tags": [
          "Libraries"
        ],
        "summary": "Library detail — categories, items, view and ordering",
        "description": "One library with its categories and their items.\n\n**The header** (`library`) is the same card `/libraries/list` returns —\nsame field names, same fallbacks — plus the settings and capability\nflags only the detail screen needs: `default_view_mode`, the two icon\nswitches, and `can_manage_items` / `can_manage_library` /\n`can_reorder_categories`. A client can therefore draw the header from\nthe row it already holds and fill in the rest.\n\n**Three header fields are manager-only and are ABSENT, not null, for\neveryone else**: `visibility`, `management_level` and `created_by`, all\ngated on `can_manage_library`. None of the three is in\n`LibraryDetailHeader.required` — read the flag, or check for the key,\nbefore binding a control to any of them. Each carries the full reason on\nits own entry below.\n\n**Each category** carries the `view` it renders in and the `sort_order`\nit sorts by — both admin-owned — plus `items_sorted_by` (the rule that\nactually applied, which differs from `sort_order` whenever `?sort=`\noverrides it) and `can_reorder_items`.\n\n**Each item** carries everything the row, the actions sheet and the\ndetails drawer read: the type and format labels, the tinted mark, the\nthumbnail, where it sits, its source state, both bylines, the file's\nformat / size / dimensions / duration, the resolved destination with its\n`mode` and per-type description, the absolute copy link, the download,\nthe caller's bookmark state, and `actions` — the kebab's option set,\nalready gated, so a client renders the menu without re-implementing a\nsingle permission rule.\n\n**The management options in that menu are web hand-offs, not API\nverbs.** This namespace is a read API with two writes (the bookmark\ntoggle and item delete) — there is no `PATCH`/`PUT`, no item-create and\nno reorder anywhere in `/api/v1/libraries`. So `actions.edit` and\n`actions.move` ship the item's `manage_url`, `can_reorder_items` ships\nthe category's, and `can_reorder_categories` uses the library's `link`:\neach flag says whether to draw the row, and the URL says where the tap\ngoes. Read `LibraryItemActions` before wiring the menu.\n\n**Empty categories are kept**, with `items: []`, so the response never\nhides the library's structure.\n\n### Fields deliberately NOT carried\n\nThe design mockup's item drawer shows a review state (`review`,\n`reviewEvery`), an expiry date, a version stack, comment and reaction\ncounts, and a page count. `library_items` has no column behind any of\nthem, so they are absent rather than fabricated — a made-up value on a\ngovernance surface is worse than a missing one. For the same reason the\nmockup's fourth sort, \"Most viewed\", is not accepted: nothing records a\nper-item view count. Passing `sort=views` falls back to `default`, and\nthe echoed `sort` says so.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Library id. Digit-constrained by the route.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "The reader's temporary page-wide order, applied to the category BLOCKS as well as to the items inside them. `default` (the default) means each category keeps its OWN `sort_order`; `az` and `recent` override every category. `updated` is accepted as an alias of `recent` for a client written against the mobile prototype, and the response always echoes the CANONICAL key. Any other value — including the prototype's unsupported `views` — falls back to `default`, exactly as the web dropdown does.",
            "schema": {
              "type": "string",
              "enum": [
                "default",
                "az",
                "recent",
                "updated"
              ],
              "default": "default"
            }
          },
          {
            "name": "category_id",
            "in": "query",
            "required": false,
            "description": "Scope the response to ONE category — how a notification or a search hit deep-links into a single section. `meta.scoped_to_category_id` echoes it, and `meta.categories_count` / `meta.items_count` narrow with it so they always describe what was returned.\n`library.categories_count` / `library.items_count` do NOT narrow: they are the library's TRUE totals, identical to what `/libraries/list` returned for the same library. See those two fields for why the pair is split this way.\nAn id that belongs to a DIFFERENT library (or to another tenant) returns an empty `categories` array rather than that library's items, and never 404s the library the caller legitimately asked for. A non-numeric value is ignored.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The library, its categories and their items",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LibraryDetail"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token"
          },
          "403": {
            "description": "`insufficient_permissions` — the token lacks `read:libraries`; or `access_denied` — the Libraries app is not accessible to this caller, either because the tenant is not entitled to it or because the caller is outside the app's per-group visibility rules."
          },
          "404": {
            "description": "No library this caller may access has that id. Returned identically for a library restricted to another audience, a disabled library the caller cannot manage, another tenant's library, and an id that does not exist — so the response cannot be used to prove a library exists. Error code `not_found`."
          }
        }
      }
    },
    "/libraries/{library_id}/items/{id}": {
      "parameters": [
        {
          "name": "library_id",
          "in": "path",
          "required": true,
          "description": "The library the item belongs to. The item is looked up THROUGH it, so this is not redundant — see the 404 notes on the operation.",
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The library ITEM id (not the library, and not the category).",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "delete": {
        "tags": [
          "Libraries"
        ],
        "summary": "Delete a library item",
        "description": "Permanently removes one item from a library — the native mirror of the\nweb item kebab's ⋯ ▸ **Delete**\n(`Apps::Libraries::ItemsController#destroy`).\n\n**Authorization — whoever the web shows the Delete entry to, and nobody\nelse.** Both surfaces ask the same predicate\n(`Libraries::Access#can_manage_items?`), so the API cannot admit someone\nthe button hides from. That predicate is:\n\n| Library `management_level` | Who may delete an item |\n|---|---|\n| *(any)* | A Libraries **app admin** — which includes every business admin and super admin — always may. This short-circuit is evaluated **before** the level. |\n| `anyone` | Anyone who can **view** the library (all-users libraries: every member; specific-audience libraries: whoever an audience rule matches, at any role). |\n| `admins_and_specific` | A member matched by an audience rule whose role is **contributor** or **manager**. A `viewer`-role rule is not enough. |\n| `admins_only` | Admins only — the audience roster is ignored entirely, so a `manager`-role rule does **not** grant it. |\n| `domain_admins_only` | Super admins (plus the admin short-circuit above). |\n\nAudience rules match on any populated target — user, department, group,\nlocation, job family, job title, organizational role, or platform role —\nso a rule naming a *role* grants the same right as one naming a person.\n\n**What the delete removes** (the model owns the cascade, so this is\nbyte-for-byte what the web's own `@item.destroy` leaves behind):\n\n* the library item row,\n* its uploaded file, if it had one (the ActiveStorage blob is purged),\n* its content-governance findings,\n* **every user's bookmark on it** — hard-deleted, not trashed. Those\n  rows are user-visible on `/bookmarks` and count toward the saved-item\n  total, so an orphan would inflate somebody's count forever.\n  `bookmarks_deleted` reports how many went.\n\nThe **library, the category and every sibling item are untouched** — an\nitem is a leaf.\n\n**Not idempotent.** A second call returns `404 not_found`, because the\nitem is genuinely gone. Clients should treat `404` on a retry as success.\n\n**Why the library id is in the path.** The item is resolved *through*\nthe named library, exactly as the web's nested route does. Pairing a\nlibrary you manage with an item id from one you do not returns `404`,\nnever a delete. For the same reason an unknown library, a library in\nanother tenant, and an item in another library are all `404` alike —\nnone of them reveals that the id exists.\n\n**Order of checks.** The permission gate runs **before** the item\nlookup, so an unauthorized caller receives `403` whether or not the item\nexists and cannot probe for valid ids by reading `403` against `404`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Item deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "item_id",
                    "deleted",
                    "message"
                  ],
                  "properties": {
                    "item_id": {
                      "type": "integer",
                      "description": "The id of the item that was deleted.",
                      "example": 4821
                    },
                    "title": {
                      "type": "string",
                      "description": "Its title, as it read at the moment of deletion.",
                      "example": "Fire drill procedure"
                    },
                    "item_type": {
                      "type": "string",
                      "description": "The kind of item it was.",
                      "enum": [
                        "simple_link",
                        "file",
                        "form",
                        "survey",
                        "wiki",
                        "post",
                        "image",
                        "video"
                      ],
                      "example": "simple_link"
                    },
                    "library_id": {
                      "type": "integer",
                      "description": "The library it was removed from.",
                      "example": 12
                    },
                    "library_name": {
                      "type": "string",
                      "example": "Employee Handbook"
                    },
                    "category_id": {
                      "type": "integer",
                      "nullable": true,
                      "description": "The category it sat in. Still present — the category is not deleted with it.",
                      "example": 34
                    },
                    "category_name": {
                      "type": "string",
                      "nullable": true,
                      "example": "Safety"
                    },
                    "bookmarks_deleted": {
                      "type": "integer",
                      "description": "How many users' bookmarks on this item were removed by the cascade. Includes bookmarks already in the platform Trash.",
                      "example": 2
                    },
                    "deleted": {
                      "type": "boolean",
                      "description": "Always true on a 200.",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Item removed from library."
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "description": "Piggybacked on every response in this API for native badge management. Unrelated to Libraries.",
                      "example": 3
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API token (`authentication_required` / `invalid_token`)."
          },
          "403": {
            "description": "Missing the `write:libraries` scope (`insufficient_permissions`); the Libraries app is not accessible to the caller (`access_denied`); or the caller may not manage items in this library (`forbidden` — \"You do not have permission to manage items in this library\")."
          },
          "404": {
            "description": "No such library in this business, no such item in that library, or the item is already gone (`not_found`). Cross-library and cross-tenant ids answer here, never 403."
          },
          "422": {
            "description": "The delete was refused by the record itself (`delete_failed`). The message carries the model's own reason."
          }
        }
      }
    },
    "/wikis/dashboard": {
      "get": {
        "tags": [
          "Wikis"
        ],
        "summary": "Wikis dashboard",
        "description": "The native-client mirror of the web Wikis dashboard. Every number and\nlist is produced by the same query object that backs the web page, so\nthe two surfaces cannot drift.\n\n**The response is persona-aware.** `is_admin` tells the client which\nshape it received:\n\n* **Every viewer** gets the `my_wikis`, `bookmarked`, `mentions` and\n  `total_views` KPIs plus `my_drafts`, `my_bookmarked_wikis` and\n  `recently_updated`. A DEPRECATED `stats.pinned` KPI and a DEPRECATED\n  `my_pinned_wikis` list mirror `stats.bookmarked` and\n  `my_bookmarked_wikis` exactly, for native builds shipped before the\n  pin→bookmark rename. Read the `bookmarked` keys; ignore the `pinned`\n  ones. `total_views.this_month` is scoped per persona —\n  for a non-admin it counts views **this month on the pages that viewer\n  created** — and a non-admin payload additionally carries\n  `total_views.total` (**all-time** views on those same pages, the\n  figure the web dashboard's \"Total Views\" card renders) and\n  `recently_viewed`.\n* **Admin-tier viewers** (business admin or above) additionally get the\n  `total_wikis` (including archived) and `stale_wikis` KPIs, a\n  `total_views.this_month` figure, the configured `stale_days` window,\n  and the `most_viewed_wikis`, `top_contributors`, `recent_activity`\n  and `needs_attention` sections. Admin-only keys are **absent** (not\n  null) for non-admins.\n\nCounts and lists are visibility-scoped: a non-admin never sees drafts\nor group-restricted wikis they could not open. Every list returns at\nmost 5 rows.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Dashboard retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "dashboard": {
                      "type": "object",
                      "required": [
                        "is_admin",
                        "stats",
                        "my_drafts",
                        "my_bookmarked_wikis",
                        "recently_updated"
                      ],
                      "properties": {
                        "is_admin": {
                          "type": "boolean",
                          "description": "True when the caller is a business admin or above. Determines whether the admin-only keys below are present.",
                          "example": true
                        },
                        "stale_days": {
                          "type": "integer",
                          "description": "Admin only. The configured stale window in days — a published wiki not updated within it counts toward `stale_wikis`.",
                          "example": 120
                        },
                        "stats": {
                          "type": "object",
                          "properties": {
                            "my_wikis": {
                              "type": "object",
                              "description": "Pages the caller authored.",
                              "properties": {
                                "total": {
                                  "type": "integer",
                                  "example": 12
                                },
                                "published": {
                                  "type": "integer",
                                  "example": 9
                                },
                                "drafts": {
                                  "type": "integer",
                                  "example": 3
                                }
                              }
                            },
                            "bookmarked": {
                              "type": "object",
                              "description": "Wikis the caller bookmarked.",
                              "properties": {
                                "total": {
                                  "type": "integer",
                                  "example": 4
                                }
                              }
                            },
                            "pinned": {
                              "type": "object",
                              "deprecated": true,
                              "description": "DEPRECATED mirror of `bookmarked` for pre-rename native builds. Always equal to it.",
                              "properties": {
                                "total": {
                                  "type": "integer",
                                  "example": 4
                                }
                              }
                            },
                            "mentions": {
                              "type": "object",
                              "description": "Wikis whose comments @mention the caller.",
                              "properties": {
                                "total": {
                                  "type": "integer",
                                  "example": 2
                                }
                              }
                            },
                            "total_views": {
                              "type": "object",
                              "description": "Page views. The SCOPE of `this_month` is persona-dependent: org-wide for admin-tier viewers, and limited to the pages the caller CREATED for everyone else. Read `is_admin` to know which you received. `total` is the ALL-TIME view sum on the pages the caller created and is present for non-admins only — there is no org-wide equivalent behind the admin reading.",
                              "properties": {
                                "this_month": {
                                  "type": "integer",
                                  "example": 348
                                },
                                "total": {
                                  "type": "integer",
                                  "example": 1290
                                }
                              }
                            },
                            "total_wikis": {
                              "type": "object",
                              "description": "Admin only. Org-wide totals by status.",
                              "properties": {
                                "total": {
                                  "type": "integer",
                                  "example": 86
                                },
                                "published": {
                                  "type": "integer",
                                  "example": 70
                                },
                                "drafts": {
                                  "type": "integer",
                                  "example": 11
                                },
                                "archived": {
                                  "type": "integer",
                                  "example": 5
                                }
                              }
                            },
                            "stale_wikis": {
                              "type": "object",
                              "description": "Admin only. Published wikis older than `stale_days`.",
                              "properties": {
                                "total": {
                                  "type": "integer",
                                  "example": 6
                                }
                              }
                            }
                          }
                        },
                        "my_drafts": {
                          "type": "array",
                          "description": "The caller's unpublished drafts, most recently edited first (max 5).",
                          "items": {
                            "allOf": [
                              {
                                "$ref": "#/components/schemas/WikiDashboardRow"
                              },
                              {
                                "type": "object",
                                "properties": {
                                  "last_edited_at": {
                                    "type": "string",
                                    "format": "date-time"
                                  }
                                }
                              }
                            ]
                          }
                        },
                        "my_bookmarked_wikis": {
                          "type": "array",
                          "description": "The caller's bookmarked wikis, most recently bookmarked first (max 5).",
                          "items": {
                            "allOf": [
                              {
                                "$ref": "#/components/schemas/WikiDashboardRow"
                              },
                              {
                                "type": "object",
                                "properties": {
                                  "views_count": {
                                    "type": "integer",
                                    "example": 128
                                  }
                                }
                              }
                            ]
                          }
                        },
                        "my_pinned_wikis": {
                          "type": "array",
                          "deprecated": true,
                          "description": "DEPRECATED mirror of `my_bookmarked_wikis` — the same cards, for native builds shipped before the pin→bookmark rename. Read `my_bookmarked_wikis`.",
                          "items": {
                            "allOf": [
                              {
                                "$ref": "#/components/schemas/WikiDashboardRow"
                              },
                              {
                                "type": "object",
                                "properties": {
                                  "views_count": {
                                    "type": "integer",
                                    "example": 128
                                  }
                                }
                              }
                            ]
                          }
                        },
                        "recently_updated": {
                          "type": "array",
                          "description": "Published wikis with the latest edits (max 5).",
                          "items": {
                            "allOf": [
                              {
                                "$ref": "#/components/schemas/WikiDashboardRow"
                              },
                              {
                                "type": "object",
                                "properties": {
                                  "last_updated_at": {
                                    "type": "string",
                                    "format": "date-time"
                                  }
                                }
                              }
                            ]
                          }
                        },
                        "recently_viewed": {
                          "type": "array",
                          "description": "Non-admin only. Wikis the caller opened most recently (max 5).",
                          "items": {
                            "allOf": [
                              {
                                "$ref": "#/components/schemas/WikiDashboardRow"
                              },
                              {
                                "type": "object",
                                "properties": {
                                  "views_count": {
                                    "type": "integer",
                                    "example": 128
                                  },
                                  "viewed_at": {
                                    "type": "string",
                                    "format": "date-time"
                                  }
                                }
                              }
                            ]
                          }
                        },
                        "most_viewed_wikis": {
                          "type": "array",
                          "description": "Admin only. Published wikis ranked by all-time views (max 5).",
                          "items": {
                            "allOf": [
                              {
                                "$ref": "#/components/schemas/WikiDashboardRow"
                              },
                              {
                                "type": "object",
                                "properties": {
                                  "views_count": {
                                    "type": "integer",
                                    "example": 1042
                                  }
                                }
                              }
                            ]
                          }
                        },
                        "top_contributors": {
                          "type": "array",
                          "description": "Admin only. Authors ranked by pages created, with the summed views of those pages (max 5).",
                          "items": {
                            "type": "object",
                            "properties": {
                              "user_id": {
                                "type": "integer",
                                "example": 42
                              },
                              "user_name": {
                                "type": "string",
                                "example": "Dana Lee"
                              },
                              "user_image": {
                                "type": "string",
                                "nullable": true,
                                "description": "Absolute avatar URL, or null when unavailable."
                              },
                              "wikis_created": {
                                "type": "integer",
                                "example": 14
                              },
                              "total_views": {
                                "type": "integer",
                                "example": 903
                              }
                            }
                          }
                        },
                        "recent_activity": {
                          "type": "array",
                          "description": "Admin only. Latest edit/publish/restore events across the business (max 5).",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 991
                              },
                              "actor_name": {
                                "type": "string",
                                "example": "Dana Lee"
                              },
                              "actor_image": {
                                "type": "string",
                                "nullable": true,
                                "description": "Absolute avatar URL, or null when unavailable."
                              },
                              "change_type": {
                                "type": "string",
                                "description": "Raw event type (WikiVersion#change_type).",
                                "enum": [
                                  "created",
                                  "updated",
                                  "minor_edit",
                                  "major_revision",
                                  "restored",
                                  "published",
                                  "ownership_changed"
                                ],
                                "example": "published"
                              },
                              "verb": {
                                "type": "string",
                                "description": "Display verb for the event, from `wikis_activity_verb`: \"created\", \"published\", \"restored\", \"revised\" (major_revision), \"made a minor edit to\" (minor_edit), \"transferred ownership of\" (ownership_changed), or \"updated\" for anything else.",
                                "example": "published"
                              },
                              "transferred_to": {
                                "type": "string",
                                "nullable": true,
                                "description": "New owner's name — present only for an ownership transfer, null otherwise."
                              },
                              "wiki_id": {
                                "type": "integer",
                                "nullable": true,
                                "example": 17
                              },
                              "wiki_title": {
                                "type": "string",
                                "nullable": true,
                                "example": "Shift Swap Guidelines"
                              },
                              "wiki_icon": {
                                "type": "string",
                                "example": "fas fa-book"
                              },
                              "wiki_color": {
                                "type": "string",
                                "example": "#3b7ddd"
                              },
                              "occurred_at": {
                                "type": "string",
                                "format": "date-time"
                              }
                            }
                          }
                        },
                        "needs_attention": {
                          "type": "array",
                          "description": "Admin only. Drafts pending publish across the business (max 5).",
                          "items": {
                            "allOf": [
                              {
                                "$ref": "#/components/schemas/WikiDashboardRow"
                              },
                              {
                                "type": "object",
                                "properties": {
                                  "creator_name": {
                                    "type": "string",
                                    "nullable": true,
                                    "example": "Dana Lee"
                                  },
                                  "last_updated_at": {
                                    "type": "string",
                                    "format": "date-time"
                                  }
                                }
                              }
                            ]
                          }
                        }
                      }
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Wikis app is not accessible to the caller — not enabled for the business, or the caller is outside the app's visibility audience (error code `access_denied`). (The Wikis API is not scope-gated.)"
          }
        }
      }
    },
    "/wikis/list": {
      "get": {
        "tags": [
          "Wikis"
        ],
        "summary": "Wikis list (\"All Wikis\")",
        "description": "The native-client mirror of the web \"All Wikis\" browse screen. Returns a\npaginated, filtered, ordered list of wikis. Visibility is scoped exactly\nlike the web: a non-admin never sees another user's drafts or\ngroup-restricted pages.\n\n**Every filter returns a FLAT list** — no row ever carries a `sub_wikis`\nkey. Each row is a card (title/color/icon) plus a `has_sub_wikis` boolean\n(does this page have at least one visible published child) so a client can\nshow an expand affordance and fetch the children lazily via\n`GET /wikis/{id}/sub_wikis`.\n\n* `all` lists published ROOT wikis only (parent_id IS NULL). Its\n  `filter_counts[\"all\"]` badge counts the WHOLE published set — roots AND\n  their sub-wikis — so it reads as the true \"total wikis\", even though the\n  list itself shows only roots. `meta.total_count` stays the ROOT count so\n  pagination over the list is correct.\n* Every OTHER filter is a flat list of matching wikis at any depth; its\n  count == its list total.\n\n**Filters** (`filter`) are persona-aware. `filter_counts` carries the\ntotal per available filter:\n\n* **Every viewer:** `all` (default — published top-level roots), `draft`\n  (a member sees only their own), `owned` (\"Mine\" — EVERYTHING the caller\n  created, sub-pages and drafts included, matching the web browse and the\n  dashboard's `my_wikis.total`), `bookmarked`, and `archived` (an admin sees\n  every archived page in the business; a member sees only the ones they\n  CREATED — the same admin/member split `draft` uses).\n* **Admin-tier viewers** additionally get `stale` (published past the\n  freshness window). A member who requests that admin-only filter is\n  served the `all` list (and `active_filter` echoes `all`).\n\n**Ordering** (`sort`): `position` (default — the admin-set Custom Order,\ni.e. the reorder target), `recent` (most recently updated), `title` (A–Z).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "filter",
            "in": "query",
            "required": false,
            "description": "all (default) | draft | owned | bookmarked | stale | archived. `stale` is admin-only. `archived` is available to every persona but role-scopes its contents (admin: the whole business; member: only the pages they created). An unknown or forbidden value falls back to `all`.\n`pinned` is a DEPRECATED alias of `bookmarked`, kept for native builds shipped before the rename; it selects the identical wikis and is echoed back verbatim in `active_filter` so a pre-rename client can still match it against its own chip. New integrations must send `bookmarked`.",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "draft",
                "owned",
                "bookmarked",
                "pinned",
                "stale",
                "archived"
              ],
              "default": "all"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "position (the admin-set Custom Order) | recent | title | popular | created. The DEFAULT is conditional, matching the web rail: `position` when `filter=all`, `recent` for every other filter. An unrecognised value falls back to `position`.",
            "schema": {
              "type": "string",
              "enum": [
                "position",
                "recent",
                "title",
                "popular",
                "created"
              ]
            }
          },
          {
            "name": "group_id",
            "in": "query",
            "required": false,
            "description": "Restrict the list to the wikis SHARED WITH ONE group — a `NotificationRecipientGroup` id, the same ids `GET /wikis/{id}` returns in its `groups` array. ORTHOGONAL to `filter`: the two compose, so `?filter=draft&group_id=7` is \"drafts inside group 7\".\n\"In the group\" means EXPLICITLY shared with it (the wiki's read visibility is restricted to that group). A wiki visible to *everyone* is readable by the group's members but is **not** in the group, so it is excluded — otherwise the filter would return nearly the whole tenant.\nThe filter only ever NARROWS: read visibility still applies, so a caller naming a group whose wikis they cannot read gets an empty list rather than a peek inside it. A group id from another business likewise matches nothing.\nA well-formed but unknown id is APPLIED (empty list, and `group_id` echoed) rather than ignored — silently returning the unfiltered list after a caller asked for one group would be misleading. A blank, non-numeric or non-positive value IS ignored, and the response echoes `group_id: null` so the two cases are distinguishable.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Wikis list retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "wikis",
                    "active_filter",
                    "sort",
                    "filter_counts",
                    "meta"
                  ],
                  "properties": {
                    "active_filter": {
                      "type": "string",
                      "description": "The filter actually applied (may differ from the request when a member asked for an admin-only filter).",
                      "example": "all"
                    },
                    "sort": {
                      "type": "string",
                      "example": "position"
                    },
                    "group_id": {
                      "type": "integer",
                      "nullable": true,
                      "description": "The group filter actually applied, or `null` when none was (a blank/non-numeric/non-positive `group_id` is ignored). Lets a client tell \"filtered to a group and it's empty\" apart from \"my group_id was ignored\".",
                      "example": 7
                    },
                    "filter_counts": {
                      "type": "object",
                      "description": "Total per available filter for this caller. Always carries all/draft/owned/bookmarked/archived; admin-tier callers also get `stale`. Every count equals its filter's list total EXCEPT `all`, which counts roots + sub-wikis (see below) while the `all` list shows only roots.\nA DEPRECATED `pinned` key mirrors `bookmarked` and is emitted immediately after it, so a native build shipped before the rename still renders its chip row in the original order. Read `bookmarked`; ignore `pinned`.\n`archived` is present for EVERY persona, but a member's count covers only the archived pages they created, while an admin's covers every archived page in the business.\nWhen `group_id` is applied these counts are narrowed to that group too, so each chip keeps counting exactly what clicking it returns inside the group.",
                      "properties": {
                        "all": {
                          "type": "integer",
                          "description": "Roots + sub-wikis (the whole published set) — the `all` list shows only roots, so this is >= meta.total_count.",
                          "example": 15
                        },
                        "draft": {
                          "type": "integer",
                          "example": 3
                        },
                        "owned": {
                          "type": "integer",
                          "example": 5
                        },
                        "bookmarked": {
                          "type": "integer",
                          "example": 2
                        },
                        "pinned": {
                          "type": "integer",
                          "example": 2,
                          "deprecated": true,
                          "description": "Mirror of `bookmarked` for pre-rename native builds. Always equal to it."
                        },
                        "stale": {
                          "type": "integer",
                          "example": 1,
                          "description": "Admin-tier only."
                        },
                        "archived": {
                          "type": "integer",
                          "example": 4,
                          "description": "Every persona. Admin: every archived page in the business; member: only the ones they created."
                        }
                      }
                    },
                    "wikis": {
                      "type": "array",
                      "description": "A FLAT list of cards — one per matching wiki. For `filter=all` these are published ROOT wikis only. No row carries a `sub_wikis` key; each carries `has_sub_wikis` and children are fetched via `GET /wikis/{id}/sub_wikis`.",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/WikiCard"
                          },
                          {
                            "type": "object",
                            "required": [
                              "has_sub_wikis"
                            ],
                            "properties": {
                              "has_sub_wikis": {
                                "type": "boolean",
                                "description": "True when this page has at least one visible published child.",
                                "example": true
                              }
                            }
                          }
                        ]
                      }
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Rows matching the filter. For `all` this is the ROOT count (the paginated list is root-only); the roots + sub-wikis grand total is `filter_counts.all`.",
                          "example": 10
                        },
                        "current_page": {
                          "type": "integer",
                          "example": 1
                        },
                        "total_pages": {
                          "type": "integer",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "example": 20
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Wikis app is not accessible to the caller — not enabled for the business, or the caller is outside the app's visibility audience (error code `access_denied`). (The Wikis API is not scope-gated.)"
          }
        }
      }
    },
    "/wikis/search": {
      "get": {
        "tags": [
          "Wikis"
        ],
        "summary": "Search wikis (by text and/or tags)",
        "description": "Search across the knowledge base — the native mirror of the web Wikis\nsearch screen PLUS the browse rail's tag chips, in one endpoint. Both\nsurfaces run the SAME shared scope, so they return the same hits in the\nsame order.\n\n**Text search (`q`)** matches `title`, `description` and `body` with a\ncase-insensitive substring match. The term is LIKE-escaped, so a literal\n`%` or `_` the user types matches literally instead of acting as a\nwildcard.\n\n**Tag search (`tags`)** filters by tag NAME, case-insensitively, against\nthis business's tag vocabulary — the same tags returned in every hit's\n`tags` array and by `GET /api/v1/wikis/{id}`. Accepts a comma-separated\nlist (`?tags=hr,policy`) or the repeated form\n(`?tags[]=hr&tags[]=policy`), up to 10 names; `tag` is accepted as an\nalias. With several tags, `match=any` (the default) returns wikis carrying\n**at least one** of them and `match=all` returns only wikis carrying\n**every** one.\n\nThe two dimensions are independent and compose with AND:\n\n| Request | Result |\n|---|---|\n| `?q=zookeeper` | text search |\n| `?tags=runbook` | browse-by-tag — a real search, **not** the blank-query no-op |\n| `?q=zookeeper&tags=runbook` | the term, narrowed to pages tagged `runbook` |\n| neither | empty result set, 200 |\n\nA tag name this business does not have is never silently ignored — a\nfilter that evaporated would hand the caller wikis that miss the tag they\njust narrowed to. With `match=any` the other, known names still apply\n(\"tagged with at least one of them\" is still satisfiable); with\n`match=all`, or when EVERY requested name is unknown, the result\n**narrows to nothing**, because no page can carry a tag this business does\nnot have. The response echoes `tags` (the canonical names actually\napplied), `unknown_tags` (names that do not exist here) and `match`, so a\nclient can tell \"filtered and empty\" apart from \"my tag was ignored\".\n\n**Visibility** mirrors the web on both dimensions: an admin-tier caller\nsearches every status (drafts and archived included); everyone else\nsearches published wikis plus their own, intersected with the app's\nread-visibility rules — so a member never finds another user's draft or a\ngroup-restricted page, tagged or not.\n\nA **blank or missing `q` with no `tags` returns an empty result set with\n200**, not an error, so a client may call this on every keystroke. Results\nare ordered newest-updated first.\n\nEach real search that carries a **term** is recorded for search analytics.\nA tag-only browse is not logged — those reports exist to surface gaps in\nwhat people typed.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Search term. Blank/omitted returns an empty result set (200) unless `tags` is supplied.",
            "schema": {
              "type": "string",
              "example": "zookeeper"
            }
          },
          {
            "name": "tags",
            "in": "query",
            "required": false,
            "description": "Tag filter. Comma-separated (`hr,policy`) or repeated (`?tags[]=hr&tags[]=policy`). Matched on tag name, case-insensitively; blanks and duplicates are dropped and at most 10 names are considered. A name this business does not have is reported in `unknown_tags`: with `match=any` the remaining known names still apply, and with `match=all` — or when every name is unknown — the result set narrows to nothing rather than widening to the names that do exist.",
            "schema": {
              "type": "string",
              "example": "hr,policy"
            }
          },
          {
            "name": "tag",
            "in": "query",
            "required": false,
            "description": "Alias for `tags`, matching the web browse rail's `?tag=` chip link. Ignored when `tags` is present and non-blank.",
            "schema": {
              "type": "string",
              "example": "hr"
            }
          },
          {
            "name": "match",
            "in": "query",
            "required": false,
            "description": "How several tags combine — `any` (tagged with at least one) or `all` (tagged with every one). Ignored when no tags are supplied; an unrecognised value falls back to `any`.",
            "schema": {
              "type": "string",
              "enum": [
                "any",
                "all"
              ],
              "default": "any"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based page number.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Hits per page, 1–50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Search executed successfully (may contain zero hits)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "query",
                    "tags",
                    "unknown_tags",
                    "match",
                    "wikis",
                    "meta"
                  ],
                  "properties": {
                    "query": {
                      "type": "string",
                      "description": "The stripped term the server searched for (echoed back).",
                      "example": "zookeeper"
                    },
                    "tags": {
                      "type": "array",
                      "description": "The tag filter as APPLIED — canonical (stored-case) tag names, de-duplicated and capped at 10. Empty when no tag filter was requested, and also empty when every requested name was unknown (in which case the result set is empty and the names appear in `unknown_tags`). Not to be confused with each hit's own `tags`.",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "hr",
                        "policy"
                      ]
                    },
                    "unknown_tags": {
                      "type": "array",
                      "description": "Requested tag names with no matching tag in this business (a typo, or a tag from another tenant). With `match=all`, or when every requested name was unknown, their presence means the result set was narrowed to nothing; with `match=any` the known names still applied.",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "hr-polices"
                      ]
                    },
                    "match": {
                      "type": "string",
                      "enum": [
                        "any",
                        "all"
                      ],
                      "description": "The multi-tag combination rule actually applied (an unrecognised `match` falls back to `any`).",
                      "example": "any"
                    },
                    "wikis": {
                      "type": "array",
                      "description": "Matching wikis, newest-updated first.",
                      "items": {
                        "$ref": "#/components/schemas/WikiSearchHit"
                      }
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "example": 7
                        },
                        "current_page": {
                          "type": "integer",
                          "example": 1
                        },
                        "total_pages": {
                          "type": "integer",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "example": 20
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Wikis app is not accessible to the caller — not enabled for the business, or the caller is outside the app's visibility audience (error code `access_denied`). (The Wikis API is not scope-gated.)"
          }
        }
      }
    },
    "/wikis/{id}/sub_wikis": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The parent wiki id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Wikis"
        ],
        "summary": "Sub-wikis of a wiki (one level)",
        "description": "Returns the immediate (ONE level) **published** sub-wikis of the given\nwiki — not the whole subtree. Each child carries its title / icon / color\nplus `has_sub_wikis` (whether that child has published children of its\nown, so a client can render an expand affordance without a second request\nper row).\n\nVisibility is scoped exactly like `GET /api/v1/wikis/list`: a non-admin\nnever sees another user's drafts or group-restricted pages. The parent\nwiki is resolved within that visible scope, so an id the caller cannot see\n(restricted, archived-away, cross-tenant, or non-existent) returns `404`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Sub-wikis retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "wiki_id",
                    "sub_wikis"
                  ],
                  "properties": {
                    "wiki_id": {
                      "type": "integer",
                      "description": "The parent wiki id (echoes the path id).",
                      "example": 42
                    },
                    "sub_wikis": {
                      "type": "array",
                      "description": "Immediate published children, ordered like the web tree (position, then title).",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "title",
                          "icon",
                          "color",
                          "bookmarked",
                          "has_sub_wikis"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 57
                          },
                          "title": {
                            "type": "string",
                            "example": "Onboarding"
                          },
                          "icon": {
                            "type": "string",
                            "description": "FontAwesome class; falls back to `fas fa-book` when unset.",
                            "example": "fas fa-door-open"
                          },
                          "color": {
                            "type": "string",
                            "description": "Whitelist-coerced safe hex; falls back to `#6c757d`.",
                            "example": "#2E7D32"
                          },
                          "bookmarked": {
                            "type": "boolean",
                            "description": "Whether the calling user has bookmarked this child wiki.",
                            "example": false
                          },
                          "has_sub_wikis": {
                            "type": "boolean",
                            "description": "True when this child has published children of its own.",
                            "example": true
                          }
                        }
                      }
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Wikis app is not accessible to the caller — not enabled for the business, or the caller is outside the app's visibility audience (error code `access_denied`). (The Wikis API is not scope-gated.)"
          },
          "404": {
            "description": "No wiki with that id is visible to the caller (error code `not_found`)."
          }
        }
      }
    },
    "/wikis/{id}/viewers": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The wiki id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Wikis"
        ],
        "summary": "People who viewed a wiki",
        "description": "Returns the distinct users who viewed the wiki, ordered by **view_count\nDESC** (most views first), with **most-recently-viewed** as the\ntiebreaker on an equal count. Aggregated per user with a real\n`view_count` and `last_viewed_at`.\n\nMirrors the web \"Viewed by N people\" modal:\n* **Authorization** — admin-tier OR the wiki's creator only. A plain\n  member viewing someone else's page must NOT learn who read it; they\n  get `403 access_denied`.\n* **Search** — `search` filters by viewer name (ILIKE), like the modal's\n  \"Search people…\" box. A `%`/`_` typed by the user matches literally.\n\nA view is deduplicated to at most one event per user per 30 minutes, so\n`view_count` is the number of distinct view sessions (not raw page loads).\nThe wiki is resolved within the caller's visible scope, so an id they\ncannot see returns `404`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Filter viewers by name (case-insensitive substring).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Viewers retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "wiki_id",
                    "viewers",
                    "meta"
                  ],
                  "properties": {
                    "wiki_id": {
                      "type": "integer",
                      "description": "The wiki id (echoes the path id).",
                      "example": 42
                    },
                    "viewers": {
                      "type": "array",
                      "description": "Distinct viewers, ordered by view_count DESC then last_viewed_at DESC.",
                      "items": {
                        "type": "object",
                        "required": [
                          "user_id",
                          "name",
                          "image_url",
                          "job_title",
                          "view_count",
                          "last_viewed_at"
                        ],
                        "properties": {
                          "user_id": {
                            "type": "integer",
                            "example": 1884
                          },
                          "name": {
                            "type": "string",
                            "example": "Georgia Fitzpatrick"
                          },
                          "image_url": {
                            "type": "string",
                            "nullable": true,
                            "description": "Absolute avatar URL (null if unavailable).",
                            "example": "https://officechat.workforce.mangoapps.com/avatars/1884.png"
                          },
                          "job_title": {
                            "type": "string",
                            "nullable": true,
                            "description": "The viewer's job title in this business (null if unset).",
                            "example": "Data Coordinator"
                          },
                          "view_count": {
                            "type": "integer",
                            "description": "Number of distinct view sessions by this user (30-min dedup).",
                            "example": 3
                          },
                          "last_viewed_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When this user most recently viewed the wiki (ISO-8601).",
                            "example": "2026-07-30T05:29:11Z"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Distinct viewer count (honors search).",
                          "example": 12
                        },
                        "current_page": {
                          "type": "integer",
                          "example": 1
                        },
                        "total_pages": {
                          "type": "integer",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "example": 20
                        }
                      }
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Wikis app is not accessible to the caller, OR the caller is neither an admin nor the wiki's creator (error code `access_denied`)."
          },
          "404": {
            "description": "No wiki with that id is visible to the caller (error code `not_found`)."
          }
        }
      }
    },
    "/wikis/{id}/comments": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The wiki id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Wikis"
        ],
        "summary": "Comment thread on a wiki",
        "description": "The wiki's comment thread, modelled on the feeds comment API. Returns a\npaginated list of TOP-LEVEL comments, each inlining its direct replies\n(threading is ONE level deep — a reply always has `replies: []`). Each\ncomment carries its author, body (with raw `@[Name](mention:id)` tokens\npreserved + a deduped `mentioned_user_ids`), a reactions summary, and its\nfile attachments.\n\nSoft-deleted comments are omitted entirely (they never appear in the\nthread or the count). `meta.total_count` counts TOP-LEVEL comments only\n(replies aren't counted). Ordering is `created_at ASC` (oldest first) for\nboth comments and their replies. The wiki is resolved within the caller's\nvisible scope, so an id they can't see returns `404`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "parent_comment_id",
            "in": "query",
            "required": false,
            "description": "When present, returns the direct REPLIES of that comment instead of the top-level thread. Resolved through this wiki's own comments, so a parent id from another wiki's thread returns an empty list.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Comment thread retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "wiki_id",
                    "comments",
                    "meta"
                  ],
                  "properties": {
                    "wiki_id": {
                      "type": "integer",
                      "description": "The wiki id (echoes the path id).",
                      "example": 42
                    },
                    "comments": {
                      "type": "array",
                      "description": "Top-level comments (oldest first), each with inlined one-level replies.",
                      "items": {
                        "$ref": "#/components/schemas/WikiComment"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "current_page": {
                          "type": "integer",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "example": 10
                        },
                        "total_count": {
                          "type": "integer",
                          "description": "Top-level comment count (excludes replies).",
                          "example": 24
                        },
                        "total_pages": {
                          "type": "integer",
                          "example": 3
                        }
                      }
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Wikis app is not accessible to the caller (error code `access_denied`)."
          },
          "404": {
            "description": "No wiki with that id is visible to the caller (error code `not_found`)."
          }
        }
      },
      "post": {
        "tags": [
          "Wikis"
        ],
        "summary": "Add a comment or reply to a wiki",
        "description": "Posts a comment on the wiki, or a reply to an existing top-level comment.\nRequires comment permission on the wiki\n(published, comments enabled, and the caller allowed by the wiki's\n`comment_permission`; otherwise 403 `comments_disabled`).\n\n**Attachments** — send files as multipart `attachments[]` (images shown\nas thumbnails, other files as download chips). Up to 5 files, 10 MB each.\n\n**Mentions** — same `@[Name](mention:id)` tokens the feeds/web composer\nemit: include them inline in `body` and the server parses them into the\ncomment's `mentioned_user_ids` and notifies each mentioned user.\n\n**Threading is ONE level.** `parent_comment_id` must reference a\nTOP-LEVEL comment of THIS wiki; replying to a reply returns 422\n`reply_depth_exceeded`, and a `parent_comment_id` from another wiki\nreturns 422 `parent_not_found`.\n\nOn success returns the created comment in the same shape as the thread\nlist rows (`{ comment: WikiComment }`).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "properties": {
                  "body": {
                    "type": "string",
                    "description": "Comment text; may contain `@[Name](mention:id)` mention tokens. Max 2000 chars.",
                    "example": "Great page @[Casey Poster](mention:49290)"
                  },
                  "parent_comment_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Top-level comment id to reply to. Omit for a new top-level comment."
                  },
                  "attachments": {
                    "type": "array",
                    "description": "Up to 5 files, 10 MB each (PNG/JPG/WEBP/GIF/PDF/TXT/CSV/ZIP/Word/Excel).",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    }
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "properties": {
                  "body": {
                    "type": "string",
                    "example": "Great page @[Casey Poster](mention:49290)"
                  },
                  "parent_comment_id": {
                    "type": "integer",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Comment created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "comment"
                  ],
                  "properties": {
                    "comment": {
                      "$ref": "#/components/schemas/WikiComment"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Wikis app is not accessible, or the wiki does not permit the caller to comment (error code `comments_disabled`)."
          },
          "404": {
            "description": "No wiki with that id is visible to the caller (error code `not_found`)."
          },
          "422": {
            "description": "Validation failed (e.g. blank body), or an invalid `parent_comment_id` — `parent_not_found` (another wiki's comment) or `reply_depth_exceeded` (replying to a reply)."
          }
        }
      }
    },
    "/wikis/{id}/reactors": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The wiki id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Wikis"
        ],
        "summary": "People who reacted to a wiki (+ per-emoji counts)",
        "description": "Returns the users who reacted to the wiki, **most-recently-reacted\nfirst**, aggregated **per user** — one row per person carrying every\nemoji they left — plus the wiki's per-emoji `reaction_counts`.\n\nMirrors the web \"who reacted\" sheet (`Platform::ReactorsController`,\nopened from the reaction bar's summary), with two improvements: the web\nreturns one row *per reaction* (a person who left 👍 and ❤️ appears\ntwice) and caps at a flat 200 rows; this endpoint aggregates per user and\npaginates.\n\n* **Authorization — anyone who can open the wiki.** Note this is\n  deliberately *looser* than the sibling `GET /wikis/{id}/viewers`, which\n  is admin/creator-only: who **viewed** a page is private, but who\n  **reacted** is public on the web to every reader. The wiki is resolved\n  within the caller's visible scope, so an id they cannot see returns\n  `404` rather than revealing that it exists.\n* **Search** — `search` filters reactors by name, first name, last name or\n  email (case-insensitive substring). A `%`/`_` typed by the user matches\n  literally. Identical to the comment-level sibling: both go through the\n  same `Platform::Reactable#reactors_scope`, so the two endpoints cannot\n  differ on which columns match.\n* **`reaction_counts` and `total_reactions` describe the WHOLE wiki and\n  are NOT narrowed by `search`** — they are the reaction bar's summary,\n  so they must not change while the user types in the people filter.\n  Only `meta.total_count` and the `reactors` list honor `search`.\n\nWikis accept the emoji set 👍 ❤️ 🎉 👀 💡 (`Wiki.reactable_emoji_set`).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Filter reactors by name / first name / last name / email (case-insensitive substring).",
            "schema": {
              "type": "string"
            },
            "example": "Dana"
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based page number (default 1).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Rows per page (default 20, maximum 50).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Reactors retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "wiki_id",
                    "reactors",
                    "reaction_counts",
                    "total_reactions",
                    "meta"
                  ],
                  "properties": {
                    "wiki_id": {
                      "type": "integer",
                      "description": "The wiki id (echoes the path id).",
                      "example": 42
                    },
                    "reaction_counts": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "integer"
                      },
                      "description": "Count per emoji for the whole wiki, e.g. `{\"👍\": 4, \"❤️\": 2}`. NOT narrowed by `search`. Empty object when nobody has reacted.",
                      "example": {
                        "👍": 4,
                        "❤️": 2
                      }
                    },
                    "total_reactions": {
                      "type": "integer",
                      "description": "Sum of `reaction_counts` — total reactions on the wiki, not the number of people. NOT narrowed by `search`.",
                      "example": 6
                    },
                    "reactors": {
                      "type": "array",
                      "description": "One row per USER, most-recently-reacted first. Honors `search` and pagination.",
                      "items": {
                        "type": "object",
                        "required": [
                          "user_id",
                          "name",
                          "job_title",
                          "image_url",
                          "reactions",
                          "reaction_count",
                          "last_reacted_at"
                        ],
                        "properties": {
                          "user_id": {
                            "type": "integer",
                            "example": 1884
                          },
                          "name": {
                            "type": "string",
                            "description": "Full name, falling back to the display name.",
                            "example": "Dana Lee"
                          },
                          "job_title": {
                            "type": "string",
                            "nullable": true,
                            "description": "The reactor's job title in this business (null if unset). Resolves both the normalized job title and the legacy free-text value.",
                            "example": "Store Manager"
                          },
                          "image_url": {
                            "type": "string",
                            "nullable": true,
                            "description": "Absolute avatar URL (null if unavailable).",
                            "example": "https://officechat.workforce.mangoapps.com/avatars/1884.png"
                          },
                          "reactions": {
                            "type": "array",
                            "description": "Every emoji this person left on the wiki, oldest first. Always non-empty.",
                            "items": {
                              "type": "string"
                            },
                            "example": [
                              "👍",
                              "❤️"
                            ]
                          },
                          "reaction_count": {
                            "type": "integer",
                            "description": "How many reactions this person left (>= 1).",
                            "example": 2
                          },
                          "last_reacted_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When this user most recently reacted (ISO-8601).",
                            "example": "2026-07-30T05:29:11Z"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Distinct reactor count (honors search).",
                          "example": 5
                        },
                        "current_page": {
                          "type": "integer",
                          "example": 1
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "0 when there are no reactors.",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "example": 20
                        }
                      }
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Wikis app is not accessible to the caller (error code `access_denied`). Unlike `/wikis/{id}/viewers`, being a non-admin is NOT a 403 here."
          },
          "404": {
            "description": "No wiki with that id is visible to the caller (error code `not_found`)."
          }
        }
      }
    },
    "/wikis/{id}/reactions": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The wiki id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Wikis"
        ],
        "summary": "Toggle the caller's emoji reaction on a wiki",
        "description": "Adds or removes the caller's emoji reaction on a wiki page — this is a\n**toggle** (the platform `Reactable#toggle_reaction` contract and the web\nreaction bar in `shared/_platform_reaction_bar`): sending an emoji the\ncaller has **not** left ADDS it; sending one they **already** left REMOVES\nit. A user may hold several DIFFERENT emojis at once — each emoji is its\nown reaction toggled independently.\n\nThe allowed emoji set is 👍 ❤️ 🎉 👀 💡 (`Wiki.reactable_emoji_set`); any\nother value returns `422 invalid_emoji`.\n\n* **Authorization — anyone who can open the wiki.** Same gate as\n  `GET /wikis/{id}/reactors` (deliberately *looser* than\n  `GET /wikis/{id}/viewers`): the reaction bar is public to every reader\n  on the web. The wiki is resolved within the caller's visible scope, so\n  an id they cannot see returns `404` rather than revealing that it\n  exists. (Not scope-gated — any authenticated caller with app access.)\n\nThe response echoes the updated reaction-bar summary\n(`reaction_counts`, `total_reactions`, `my_reactions`) so the client can\nrepaint the bar without a follow-up `GET`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emoji"
                ],
                "properties": {
                  "emoji": {
                    "type": "string",
                    "description": "One of the allowed emoji (👍 ❤️ 🎉 👀 💡).",
                    "example": "👍"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reaction toggled. `reacted` is `true` when the caller's reaction is now present, `false` when it was removed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "wiki_id",
                    "emoji",
                    "reacted",
                    "reaction_counts",
                    "total_reactions",
                    "my_reactions"
                  ],
                  "properties": {
                    "wiki_id": {
                      "type": "integer",
                      "description": "The wiki id (echoes the path id).",
                      "example": 42
                    },
                    "emoji": {
                      "type": "string",
                      "description": "The emoji that was toggled.",
                      "example": "👍"
                    },
                    "reacted": {
                      "type": "boolean",
                      "description": "True if the caller's reaction is now present; false if it was removed.",
                      "example": true
                    },
                    "reaction_counts": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "integer"
                      },
                      "description": "Count per emoji for the whole wiki after the toggle, e.g. `{\"👍\": 4, \"❤️\": 2}`. Empty object when nobody has reacted.",
                      "example": {
                        "👍": 4,
                        "❤️": 2
                      }
                    },
                    "total_reactions": {
                      "type": "integer",
                      "description": "Sum of `reaction_counts` — total reactions on the wiki (not the number of people).",
                      "example": 6
                    },
                    "my_reactions": {
                      "type": "array",
                      "description": "The emojis the CALLER currently has on this wiki.",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "👍"
                      ]
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Wikis app is not accessible to the caller (error code `access_denied`)."
          },
          "404": {
            "description": "No wiki with that id is visible to the caller (error code `not_found`)."
          },
          "422": {
            "description": "The `emoji` is missing (error code `emoji_required`) or outside the allowed set (error code `invalid_emoji`; the response `error.details.allowed` lists the accepted emoji)."
          }
        }
      }
    },
    "/wikis/{id}/bookmark": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The wiki id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Wikis"
        ],
        "summary": "Bookmark a wiki for the current user",
        "description": "Bookmarks the wiki for the **calling user**, writing the platform-wide\n`Platform::Bookmark` shared with the `/bookmarks` page and the web\nbookmark button. **Idempotent** — bookmarking an already-bookmarked wiki\nis a no-op that still returns `bookmarked: true` and never creates a\nsecond row.\n\n**Supersedes `POST /wikis/{id}/pin`**, which is still served as a\ndeprecated alias of this endpoint for native builds shipped before the\nrename (same rows, same contract). The Wikis app used to carry an\napp-local pin alongside Bookmarks — two ways to mark the same page — and\nthe pin was removed. Clients calling the old path must move to this one.\n\n* **Authorization — anyone who can open the wiki.** The wiki is resolved\n  within the caller's visible scope, so an id they cannot see returns\n  `404` rather than revealing that it exists. Not scope-gated: any\n  authenticated caller with Wikis-app access may bookmark.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Wiki bookmarked (or already bookmarked).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "wiki_id",
                    "bookmarked"
                  ],
                  "properties": {
                    "wiki_id": {
                      "type": "integer",
                      "description": "The wiki id (echoes the path id).",
                      "example": 42
                    },
                    "bookmarked": {
                      "type": "boolean",
                      "description": "The bookmark state for the caller AFTER this call (always `true` here).",
                      "example": true
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Wikis app is not accessible to the caller (error code `access_denied`)."
          },
          "404": {
            "description": "No wiki with that id is visible to the caller (error code `not_found`)."
          }
        }
      },
      "delete": {
        "tags": [
          "Wikis"
        ],
        "summary": "Remove the current user's bookmark on a wiki",
        "description": "Removes the calling user's bookmark on the wiki. **Idempotent** —\nremoving a bookmark that isn't there is a no-op that still returns\n`bookmarked: false`. Same authorization as the POST verb.\n\nA bookmark carrying a note is soft-deleted (recoverable from the\nplatform Trash); a bare one is removed outright, matching the web\ntoggle.\n\n**Supersedes `DELETE /wikis/{id}/pin`**, which is still served as a\ndeprecated alias of this endpoint for native builds shipped before the\nrename (same rows, same contract).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Bookmark removed (or there was none).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "wiki_id",
                    "bookmarked"
                  ],
                  "properties": {
                    "wiki_id": {
                      "type": "integer",
                      "description": "The wiki id (echoes the path id).",
                      "example": 42
                    },
                    "bookmarked": {
                      "type": "boolean",
                      "description": "The bookmark state for the caller AFTER this call (always `false` here).",
                      "example": false
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Wikis app is not accessible to the caller (error code `access_denied`)."
          },
          "404": {
            "description": "No wiki with that id is visible to the caller (error code `not_found`)."
          }
        }
      }
    },
    "/wikis/{id}/archive": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The wiki id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Wikis"
        ],
        "summary": "Archive a wiki",
        "description": "Archives the wiki — sets its status to `archived`, mirroring the web\n\"Archive\" action (Apps::Wikis::PagesController#update with\n`wiki[status]=archived`). There is no discard/soft-delete: an archived\nwiki drops out of the default browse tree but remains restorable.\n**Idempotent** — archiving an already-archived wiki is a no-op that\nstill returns status `archived`.\n\n* **Authorization — the web's status-change gate, faithfully** (its\n  `can_manage_wiki?` AND `can_edit_wiki?`, the same pair the unarchive\n  twin below documents): a business **admin (or above)** may always\n  archive; the wiki's **creator** may archive **only while the wiki is\n  not locked** (a locked wiki fails `can_edit_wiki?` for a non-admin).\n  Any other caller who can see the wiki gets `403` (`forbidden`); a\n  caller who cannot even see the wiki gets `404` (resolved out by the\n  visible scope, never revealing it exists). This is a per-wiki check,\n  NOT a token scope.\n* **A wiki someone else is actively editing answers `409`**, not `403` —\n  the lock is transient, so retrying after it clears succeeds.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Wiki archived (or already archived).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "wiki_id",
                    "status"
                  ],
                  "properties": {
                    "wiki_id": {
                      "type": "integer",
                      "description": "The wiki id (echoes the path id).",
                      "example": 42
                    },
                    "status": {
                      "type": "string",
                      "description": "The wiki's status after the call.",
                      "enum": [
                        "archived"
                      ],
                      "example": "archived"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller may not archive this wiki — they are neither a business admin nor its (unlocked-wiki) creator (error code `forbidden`), or the Wikis app is not accessible to them (error code `access_denied`)."
          },
          "404": {
            "description": "No wiki with that id is visible to the caller (error code `not_found`)."
          },
          "409": {
            "description": "Another user currently holds the edit lock on this wiki (error code `wiki_being_edited`); the message names the lock holder. Distinct from `403` because the condition is transient — retry once the lock clears."
          }
        }
      },
      "delete": {
        "tags": [
          "Wikis"
        ],
        "summary": "Unarchive a wiki",
        "description": "Unarchives the wiki — sets its status back to `draft`, mirroring the web\n\"Restore to Draft\" action (Apps::Wikis::PagesController#update with\n`wiki[status]=draft`). It restores to **draft** (NOT `published`), so the\nowner can review before re-publishing. **Idempotent** — calling it on a\nwiki that isn't archived is a no-op that returns the wiki's current\nstatus unchanged.\n\n* **Authorization — the web's status-change gate, faithfully** (its\n  `can_manage_wiki?` AND `can_edit_wiki?`): a business **admin (or above)**\n  may always unarchive; the wiki's **creator** may unarchive **only while\n  the wiki is not locked** (a locked wiki fails `can_edit_wiki?` for a\n  non-admin, so a locked archived page can be restored by an admin only).\n  Any other caller who can see the wiki gets `403` (`forbidden`); a caller\n  who cannot even see it gets `404`. Note that an archived wiki is visible\n  only to admins and its creator, so a non-creator member typically gets\n  `404` here rather than `403`. This is a per-wiki check, NOT a token scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Wiki unarchived (or already not archived).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "wiki_id",
                    "status"
                  ],
                  "properties": {
                    "wiki_id": {
                      "type": "integer",
                      "description": "The wiki id (echoes the path id).",
                      "example": 42
                    },
                    "status": {
                      "type": "string",
                      "description": "The wiki's status after the call — `draft` after restoring an archived wiki, or its unchanged current status on a no-op.",
                      "enum": [
                        "draft",
                        "published"
                      ],
                      "example": "draft"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller may not unarchive this wiki — they are neither a business admin nor its (unlocked-wiki) creator (error code `forbidden`), or the Wikis app is not accessible to them (error code `access_denied`)."
          },
          "404": {
            "description": "No wiki with that id is visible to the caller (error code `not_found`)."
          },
          "409": {
            "description": "Another user currently holds the edit lock on this wiki (error code `wiki_being_edited`); the message names the lock holder. Distinct from `403` because the condition is transient — retry once the lock clears."
          }
        }
      }
    },
    "/wikis/{wiki_id}/comments/{id}/reactions": {
      "parameters": [
        {
          "name": "wiki_id",
          "in": "path",
          "required": true,
          "description": "The wiki id.",
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The comment id. A reply is the same comment model (threaded), so this handles comments AND replies.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Wikis"
        ],
        "summary": "Toggle the caller's emoji reaction on a wiki comment or reply",
        "description": "Adds or removes the caller's emoji reaction on a wiki **comment** — or a\n**reply**, which is the same threaded comment model, so this one endpoint\nhandles both. This is a **toggle** (the platform\n`Reactable#toggle_reaction` contract and the web comment reaction bar in\n`Apps::Wikis::CommentsController#react`): sending an emoji the caller has\n**not** left ADDS it; sending one they **already** left REMOVES it. A user\nmay hold several DIFFERENT emojis at once — each is toggled independently.\n\nThe allowed emoji set is 👍 ❤️ 🎉 👀 💡 (`WikiComment.reactable_emoji_set`);\nany other value returns `422 invalid_emoji`.\n\n* **Authorization — anyone who can open the wiki** (same as the page\n  reaction bar). The wiki is resolved within the caller's visible scope,\n  and the comment is resolved THROUGH that wiki's own thread, so a\n  restricted / cross-tenant / foreign / missing id all return `404`\n  rather than revealing that it exists. (Not scope-gated.)\n\nThe response echoes the updated reaction-bar summary for the comment\n(`reaction_counts`, `total_reactions`, `my_reactions`) so the client can\nrepaint the bar without a follow-up `GET`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emoji"
                ],
                "properties": {
                  "emoji": {
                    "type": "string",
                    "description": "One of the allowed emoji (👍 ❤️ 🎉 👀 💡).",
                    "example": "👍"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reaction toggled. `reacted` is `true` when the caller's reaction is now present, `false` when it was removed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "wiki_id",
                    "comment_id",
                    "emoji",
                    "reacted",
                    "reaction_counts",
                    "total_reactions",
                    "my_reactions"
                  ],
                  "properties": {
                    "wiki_id": {
                      "type": "integer",
                      "description": "The wiki id (echoes the path).",
                      "example": 42
                    },
                    "comment_id": {
                      "type": "integer",
                      "description": "The comment (or reply) id (echoes the path).",
                      "example": 57
                    },
                    "emoji": {
                      "type": "string",
                      "description": "The emoji that was toggled.",
                      "example": "👍"
                    },
                    "reacted": {
                      "type": "boolean",
                      "description": "True if the caller's reaction is now present; false if it was removed.",
                      "example": true
                    },
                    "reaction_counts": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "integer"
                      },
                      "description": "Count per emoji for the comment after the toggle, e.g. `{\"👍\": 4, \"❤️\": 2}`. Empty object when nobody has reacted.",
                      "example": {
                        "👍": 4,
                        "❤️": 2
                      }
                    },
                    "total_reactions": {
                      "type": "integer",
                      "description": "Sum of `reaction_counts` — total reactions on the comment.",
                      "example": 6
                    },
                    "my_reactions": {
                      "type": "array",
                      "description": "The emojis the CALLER currently has on this comment.",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "👍"
                      ]
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Wikis app is not accessible to the caller (error code `access_denied`)."
          },
          "404": {
            "description": "No wiki with that id is visible to the caller, or no such comment on that wiki (error code `not_found`)."
          },
          "422": {
            "description": "The `emoji` is missing (error code `emoji_required`) or outside the allowed set (error code `invalid_emoji`; the response `error.details.allowed` lists the accepted emoji)."
          }
        }
      }
    },
    "/wikis/{wiki_id}/comments/{id}/reactors": {
      "parameters": [
        {
          "name": "wiki_id",
          "in": "path",
          "required": true,
          "description": "The wiki id.",
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The comment id. A reply is the same comment model (threaded), so this handles comments AND replies.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Wikis"
        ],
        "summary": "People who reacted to a wiki comment (+ per-emoji counts)",
        "description": "Returns the users who reacted to one wiki **comment or reply**,\n**most-recently-reacted first**, aggregated **per user** — one row per\nperson carrying every emoji they left — plus that comment's per-emoji\n`reaction_counts`.\n\nThis is the read side of `POST /wikis/{wiki_id}/comments/{id}/reactions`,\nwhich reports only the caller's own state and the totals. It returns the\n**same row and `meta` shape** as the page-level\n`GET /wikis/{id}/reactors`, so one client parser handles both.\n\n* **Replies are the same endpoint.** A reply is the same comment model\n  (threaded via `parent_comment_id`, still attached to the wiki), so pass\n  either a top-level comment id or a reply id. A reply's reactors are\n  independent of its parent's.\n* **Authorization — anyone who can open the wiki.** Matching both the web\n  comment reaction bar and the page-level reactors endpoint, and\n  deliberately *looser* than `GET /wikis/{id}/viewers`, which is\n  admin/creator-only: who **viewed** a page is private, who **reacted** is\n  not. The wiki is resolved within the caller's visible scope and the\n  comment through that wiki's own thread, so a restricted, cross-tenant,\n  cross-wiki or missing id all return `404` alike — never revealing that\n  something the caller cannot see exists.\n* **Search** — `search` filters reactors by name, first name, last name or\n  email (case-insensitive substring). A `%`/`_` typed by the user matches\n  literally.\n* **`reaction_counts` and `total_reactions` describe the WHOLE comment and\n  are NOT narrowed by `search`** — they are the comment's reaction-bar\n  summary, so they must not change while the user types in the people\n  filter. Only `meta.total_count` and the `reactors` list honor `search`.\n* **Counts are scoped to this comment** — a reaction on the wiki *page*, or\n  on a sibling comment or reply, never appears here.\n\nComments accept the emoji set 👍 ❤️ 🎉 👀 💡\n(`WikiComment.reactable_emoji_set`).\n\nQuery cost is flat at 5 queries regardless of page size — every field a row\ncarries is resolved from a preloaded batch.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Filter reactors by name / first name / last name / email (case-insensitive substring).",
            "schema": {
              "type": "string"
            },
            "example": "Dana"
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based page number (default 1).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Rows per page (default 20, maximum 50).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Comment reactors retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "wiki_id",
                    "comment_id",
                    "reactors",
                    "reaction_counts",
                    "total_reactions",
                    "meta"
                  ],
                  "properties": {
                    "wiki_id": {
                      "type": "integer",
                      "description": "The wiki id (echoes the path `wiki_id`).",
                      "example": 42
                    },
                    "comment_id": {
                      "type": "integer",
                      "description": "The comment (or reply) id (echoes the path `id`).",
                      "example": 907
                    },
                    "reaction_counts": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "integer"
                      },
                      "description": "Count per emoji for this COMMENT, e.g. `{\"👍\": 4, \"❤️\": 2}`. NOT narrowed by `search`, and never includes reactions left on the wiki page or on a sibling comment/reply. Empty object when nobody has reacted.",
                      "example": {
                        "👍": 4,
                        "❤️": 2
                      }
                    },
                    "total_reactions": {
                      "type": "integer",
                      "description": "Sum of `reaction_counts` — total reactions on this comment, not the number of people. NOT narrowed by `search`.",
                      "example": 6
                    },
                    "reactors": {
                      "type": "array",
                      "description": "One row per USER, most-recently-reacted first. Honors `search` and pagination.",
                      "items": {
                        "type": "object",
                        "required": [
                          "user_id",
                          "name",
                          "job_title",
                          "image_url",
                          "reactions",
                          "reaction_count",
                          "last_reacted_at"
                        ],
                        "properties": {
                          "user_id": {
                            "type": "integer",
                            "example": 1884
                          },
                          "name": {
                            "type": "string",
                            "description": "Full name, falling back to the display name. `\"Unknown\"` for a reaction left by someone no longer an active member of this business.",
                            "example": "Dana Lee"
                          },
                          "job_title": {
                            "type": "string",
                            "nullable": true,
                            "description": "The reactor's job title in this business (null if unset). Resolves both the normalized job title and the legacy free-text value.",
                            "example": "Store Manager"
                          },
                          "image_url": {
                            "type": "string",
                            "nullable": true,
                            "description": "Absolute avatar URL (null if unavailable).",
                            "example": "https://officechat.workforce.mangoapps.com/avatars/1884.png"
                          },
                          "reactions": {
                            "type": "array",
                            "description": "Every emoji this person left on THIS comment, oldest first. Always non-empty.",
                            "items": {
                              "type": "string"
                            },
                            "example": [
                              "👍",
                              "❤️"
                            ]
                          },
                          "reaction_count": {
                            "type": "integer",
                            "description": "How many reactions this person left (>= 1).",
                            "example": 2
                          },
                          "last_reacted_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When this user most recently reacted to this comment (ISO-8601).",
                            "example": "2026-07-30T05:29:11Z"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Distinct reactor count (honors search).",
                          "example": 5
                        },
                        "current_page": {
                          "type": "integer",
                          "example": 1
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "0 when there are no reactors.",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "example": 20
                        }
                      }
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Wikis app is not accessible to the caller (error code `access_denied`). Unlike `/wikis/{id}/viewers`, being a non-admin is NOT a 403 here."
          },
          "404": {
            "description": "No wiki with that id is visible to the caller, or no such comment on that wiki (error code `not_found`)."
          }
        }
      }
    },
    "/wikis/{wiki_id}/comments/{id}": {
      "parameters": [
        {
          "name": "wiki_id",
          "in": "path",
          "required": true,
          "description": "The wiki id.",
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The comment id. A reply is the same threaded comment model, so this handles comments AND replies.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "patch": {
        "tags": [
          "Wikis"
        ],
        "summary": "Edit a wiki comment or reply",
        "description": "Edits the body of a comment (or reply — the same threaded model). Allowed\nfor the **author within 15 minutes** of posting, or an **admin** of the\nwiki's business at any time; otherwise 403 `cannot_edit`.\n\n**Mentions** are re-parsed from the new `body` — the same\n`@[Name](mention:id)` tokens as create — so `mentioned_user_ids` reflects\nthe edited text. **Attachments** sent as multipart `attachments[]` are\nAPPENDED to the comment (existing attachments are kept). The edit stamps\n`edited_at`, which the read API exposes so clients can show an \"edited\"\nmarker. No notifications are fired on edit.\n\nOn success returns the updated comment in the same shape as the thread\nlist rows (`{ comment: WikiComment }`).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "body": {
                    "type": "string",
                    "description": "New comment text; may contain `@[Name](mention:id)` mention tokens. Max 2000 chars.",
                    "example": "Updated — thanks @[Casey Poster](mention:49290)"
                  },
                  "attachments": {
                    "type": "array",
                    "description": "Additional files to append (up to 5, 10 MB each).",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    }
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "body": {
                    "type": "string",
                    "example": "Updated — thanks @[Casey Poster](mention:49290)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Comment updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "comment"
                  ],
                  "properties": {
                    "comment": {
                      "$ref": "#/components/schemas/WikiComment"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Wikis app is not accessible, or the caller may no longer edit this comment — not the author, or past the 15-minute edit window (error code `cannot_edit`)."
          },
          "404": {
            "description": "No wiki with that id is visible to the caller, or no comment with that id exists on this wiki (error code `not_found`)."
          },
          "422": {
            "description": "Validation failed (e.g. blank body)."
          }
        }
      },
      "delete": {
        "tags": [
          "Wikis"
        ],
        "summary": "Delete a wiki comment or reply",
        "description": "Soft-deletes a comment (or reply — the same threaded model). Allowed for\nthe **author within 5 minutes** of posting, or an **admin** of the wiki's\nbusiness at any time; otherwise 403 `cannot_delete`.\n\nThe delete is soft: the row leaves the thread immediately (subsequent GETs\nomit it and it stops counting toward `meta.total_count`) but is retained\nfor the audit window. Deleting a top-level comment removes its replies from\nthe thread as well.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Comment deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "deleted"
                  ],
                  "properties": {
                    "id": {
                      "type": "integer",
                      "example": 8842
                    },
                    "deleted": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Wikis app is not accessible, or the caller may no longer delete this comment — not the author, or past the 5-minute delete window (error code `cannot_delete`)."
          },
          "404": {
            "description": "No wiki with that id is visible to the caller, or no comment with that id exists on this wiki (error code `not_found`)."
          }
        }
      }
    },
    "/wikis/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The wiki id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Wikis"
        ],
        "summary": "Full detail for one wiki (records a view)",
        "description": "Full detail for a single wiki — the native mirror of the web reader pane\n(`apps/wikis/shared/_wiki_viewer`).\n\n* **Authorization — only users who may VIEW the wiki.** An admin sees any\n  status; a member sees a published wiki visible to them (everyone, a\n  group they belong to, or their own page of any status). Any wiki they\n  cannot view — restricted, another user's draft, another tenant's, or a\n  missing id — returns `404` identically, so the endpoint never reveals\n  that a wiki exists.\n* **View count.** Like the web reader, a successful read records a\n  `WikiViewEvent` (deduplicated to one per user per 30 minutes) and\n  increments `views_count`. The `views_count` in THIS response is the\n  pre-increment figure (the view is recorded after the payload is built).\n* `can_archive` / `can_delete` reflect the web `can_manage_wiki?` gate:\n  `true` for a business admin-or-above OR the wiki's creator.\n  `can_archive` is additionally `false` when the wiki is already archived.\n* `comments_enabled` is whether the page is open to comments at all,\n  independent of the caller — `false` only when **\"Who can comment?\"** is\n  `nobody`. `can_comment` resolves that same setting for THIS caller, folding\n  in publish state; it is the same predicate the write path enforces, so a\n  comment composer rendered from it can never 403 on submit. The two together\n  tell \"comments are off for this page\" apart from \"you specifically may not\n  comment\". (The stored **\"Enable comments\"** master toggle that\n  `comments_enabled` used to report was removed as a duplicate of\n  **\"Who can comment?\"**; the field is unchanged in name, type and meaning\n  and is now derived from that setting.)\n* `author` is the byline, honouring the page's **\"Show Author\"** setting\n  (null when off); `creator` is the ownership fact and is always present.\n* `show_toc` is the page's stored **\"Show Table of Contents\"** setting; the\n  ToC entries themselves are not part of this payload.\n* `reaction_counts` is the per-emoji breakdown; `total_reactions` its\n  sum; `distinct_reactions` the number of distinct emoji;\n  `current_user_reactions` the emoji the CALLER left.\n* `bookmarked` is whether the **CALLER** has bookmarked this wiki (a\n  bookmark is per-user, so it is never \"somebody bookmarked it\"). It is\n  the same key `POST` / `DELETE /wikis/{id}/bookmark` return, so the\n  toggle response can be\n  written straight back onto a detail you are holding.\n* `parents` is the wiki's ancestor chain for a breadcrumb — **root first**,\n  excluding the wiki itself, `[]` for a top-level wiki. Ancestors the\n  caller cannot view are **omitted**, so the chain can have gaps: a\n  published page may hang under another user's draft, and crumbing it\n  would hand the caller the title of a page this endpoint would itself\n  `404`. Combine with `sub_wikis` to place the wiki in the tree —\n  `parents` looks up, `sub_wikis` looks down.\n* `sub_wikis` is **capped at 200 nodes**, cut breadth-first so the tree you\n  get is always connected (a node appears only if its parent does).\n  `sub_wikis_total` is how many nodes the subtree really holds and\n  `sub_wikis_truncated` says whether the cap bit; both are additive, so a\n  client that ignores them is unaffected. Nothing shipping today reaches\n  the cap — the largest subtree measured across the fleet is well under it\n  — it bounds a response that otherwise grows with nothing but how many\n  sub-pages someone filed under one wiki.\n* **Deleting is reversible and the API owns both halves:**\n  `DELETE /wikis/{id}` moves the page and its sub-tree to Trash,\n  `GET /wikis/trash` lists what is in there and\n  `POST /wikis/{id}/restore` brings a page (and its cascade) back.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Wiki detail retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "wiki"
                  ],
                  "properties": {
                    "wiki": {
                      "type": "object",
                      "required": [
                        "id",
                        "title",
                        "image_url",
                        "web_view_url",
                        "copy_link_url",
                        "status",
                        "is_archived",
                        "last_updated_at",
                        "current_version",
                        "comments_count",
                        "views_count",
                        "creator",
                        "show_author",
                        "author",
                        "groups",
                        "tags",
                        "show_toc",
                        "can_archive",
                        "can_delete",
                        "comments_enabled",
                        "can_comment",
                        "total_reactions",
                        "distinct_reactions",
                        "reaction_counts",
                        "current_user_reactions",
                        "bookmarked",
                        "attachments",
                        "parents",
                        "sub_wikis",
                        "sub_wikis_total",
                        "sub_wikis_truncated"
                      ],
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 42
                        },
                        "title": {
                          "type": "string",
                          "example": "Apache Zookeeper"
                        },
                        "image_url": {
                          "type": "string",
                          "nullable": true,
                          "description": "Absolute cover-image URL (null when none is attached).",
                          "example": "https://officechat.workforce.mangoapps.com/rails/active_storage/blobs/redirect/x/cover.png"
                        },
                        "web_view_url": {
                          "type": "string",
                          "nullable": true,
                          "description": "Absolute URL of the MOBILE web view (`/m/apps/wikis/:id`) — the bare, description-only reader meant to be embedded in a native WebView. Always carries `?embed=1`, which is what strips the web chrome; load this url VERBATIM. Dropping the param falls back to User-Agent detection, and a WebView whose UA carries no MangoApps token then renders the full mobile chrome (a second top bar plus the bottom tab bar) inside the native screen.",
                          "example": "https://officechat.workforce.mangoapps.com/m/apps/wikis/42?embed=1"
                        },
                        "copy_link_url": {
                          "type": "string",
                          "nullable": true,
                          "description": "Absolute shareable permalink to this page — the same link the web reader's \"Copy link\" button copies (`/apps/wikis/pages/:id`).",
                          "example": "https://officechat.workforce.mangoapps.com/apps/wikis/pages/42"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "published",
                            "archived"
                          ],
                          "example": "published"
                        },
                        "is_archived": {
                          "type": "boolean",
                          "description": "Whether the wiki is archived — `status == \"archived\"` as a boolean, so a client can branch without string-matching. Pair it with `can_archive`, which is false both for an already-archived page and for a caller who may not archive: `is_archived` is what tells you whether to offer **Archive** or **Unarchive** (`POST` / `DELETE /wikis/{id}/archive`).",
                          "example": false
                        },
                        "last_updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "When the wiki was last updated (ISO-8601).",
                          "example": "2026-07-30T05:29:11Z"
                        },
                        "current_version": {
                          "type": "integer",
                          "description": "Highest version number recorded (0 when the wiki has no versions).",
                          "example": 3
                        },
                        "comments_count": {
                          "type": "integer",
                          "description": "Number of (non-deleted) TOP-LEVEL comments on the wiki. Replies are NOT counted — every implementation (details#show and all three web call sites) computes `wiki_comments.top_level.count`, and this line said \"replies included\" until 2026-09-05. The thread endpoint's own `meta.total_count` uses the same rule, so the two agree.\n\n**`0` whenever `comments_enabled` is false.** \"Who can comment? → Nobody\" retracts the thread everywhere — the web reader replaces it with \"Comments are disabled on this wiki.\" and `GET /wikis/{id}/comments` returns an empty list — so this counts what the app will actually show, never a retracted thread. The two fields can no longer contradict each other in one response.",
                          "example": 5
                        },
                        "views_count": {
                          "type": "integer",
                          "description": "Total views (pre-increment for this read).",
                          "example": 128
                        },
                        "creator": {
                          "type": "object",
                          "required": [
                            "id",
                            "name"
                          ],
                          "properties": {
                            "id": {
                              "type": "integer",
                              "nullable": true,
                              "example": 1884
                            },
                            "name": {
                              "type": "string",
                              "example": "Dana Lee"
                            }
                          }
                        },
                        "show_author": {
                          "type": "boolean",
                          "description": "The page's own \"Show Author\" setting (Properties). When false the web reader prints no byline — and `author` below is null.",
                          "example": true
                        },
                        "author": {
                          "type": "object",
                          "nullable": true,
                          "description": "The byline to render: the same person as `creator`, but **null when `show_author` is false**. Use this for the byline and `creator` for ownership — the author can hide the byline without the page becoming ownerless, so `creator` is always reported.",
                          "required": [
                            "id",
                            "name"
                          ],
                          "properties": {
                            "id": {
                              "type": "integer",
                              "nullable": true,
                              "example": 1884
                            },
                            "name": {
                              "type": "string",
                              "example": "Dana Lee"
                            }
                          }
                        },
                        "groups": {
                          "type": "array",
                          "description": "Visibility groups gating the wiki (empty for everyone / just_me visibility).",
                          "items": {
                            "type": "object",
                            "required": [
                              "id",
                              "name"
                            ],
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 7
                              },
                              "name": {
                                "type": "string",
                                "example": "Engineering"
                              }
                            }
                          }
                        },
                        "tags": {
                          "type": "array",
                          "description": "The wiki's tags, sorted by name.",
                          "items": {
                            "type": "object",
                            "required": [
                              "id",
                              "name"
                            ],
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 12
                              },
                              "name": {
                                "type": "string",
                                "example": "devops"
                              }
                            }
                          }
                        },
                        "can_archive": {
                          "type": "boolean",
                          "description": "May the caller archive this wiki (admin or creator, and not already archived).",
                          "example": true
                        },
                        "can_delete": {
                          "type": "boolean",
                          "description": "May the caller delete this wiki (admin or creator).",
                          "example": true
                        },
                        "comments_enabled": {
                          "type": "boolean",
                          "description": "Whether the page is open to comments at all, independent of the caller — `false` only when **\"Who can comment?\"** is `nobody`, which is how a page closes comments. Use `can_comment` for whether THIS caller may actually post; the two differ on a page that is open but restricted (`admin_and_creator`). On a `nobody` page they agree: `comments_enabled: false` forces `can_comment: false` for **every** caller, admins included — the closed-thread check runs BEFORE the admin override, matching the web reader, which replaces the whole thread with \"Comments are disabled on this wiki.\" for every persona. Formerly the stored **\"Enable comments\"** master toggle, removed as a duplicate of **\"Who can comment?\"** and now derived from it — same name, type and meaning.",
                          "example": true
                        },
                        "can_comment": {
                          "type": "boolean",
                          "description": "May the CALLER post a comment. `true` only when the wiki is **published** and the page's **\"Who can comment?\"** setting admits them (`any_user` → anyone, `admin_and_creator` → the creator, `nobody` → **no one, including admins**). The `nobody` check runs BEFORE the admin override, so an admin does NOT keep a composer on a page closed to comments; on every other setting an admin bypasses the restriction. This is the exact predicate `POST /wikis/{id}/comments` enforces, so a composer rendered on this flag can never be refused on submit.",
                          "example": true
                        },
                        "show_toc": {
                          "type": "boolean",
                          "description": "The page's stored **\"Show Table of Contents\"** setting. The ToC entries themselves are not part of this payload — this flag only mirrors the author's choice.",
                          "example": true
                        },
                        "total_reactions": {
                          "type": "integer",
                          "description": "Total reactions on the wiki (sum of reaction_counts).",
                          "example": 6
                        },
                        "distinct_reactions": {
                          "type": "integer",
                          "description": "Number of distinct emoji used (keys of reaction_counts).",
                          "example": 3
                        },
                        "reaction_counts": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "integer"
                          },
                          "description": "Count per emoji (e.g. four 👍 and two ❤️). Empty when nobody has reacted.",
                          "example": {
                            "👍": 4,
                            "❤️": 2
                          }
                        },
                        "current_user_reactions": {
                          "type": "array",
                          "description": "The emoji the CALLER left on this wiki (empty if none).",
                          "items": {
                            "type": "string"
                          },
                          "example": [
                            "👍"
                          ]
                        },
                        "bookmarked": {
                          "type": "boolean",
                          "description": "Whether the CALLER has bookmarked this wiki. A bookmark is per-user, so this is the caller's own state and never \"somebody bookmarked it\" — render the reader's bookmark toggle from it on first paint. Same key POST/DELETE /wikis/{id}/bookmark return. Supersedes the former `pinned` key, which is still mirrored below for pre-rename builds.",
                          "example": true
                        },
                        "pinned": {
                          "type": "boolean",
                          "deprecated": true,
                          "description": "DEPRECATED mirror of `bookmarked`, kept for native builds shipped before the pin→bookmark rename. Computed from the same predicate, so the two can never disagree. New integrations read `bookmarked`.",
                          "example": true
                        },
                        "attachments": {
                          "type": "array",
                          "description": "The wiki's page-level attachments (max 5). Each carries an absolute `url` and `download_url`; `is_image` is true for images the client can render inline and false for download chips.",
                          "items": {
                            "type": "object",
                            "required": [
                              "id",
                              "filename",
                              "content_type",
                              "byte_size",
                              "is_image",
                              "url",
                              "download_url"
                            ],
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 2633
                              },
                              "filename": {
                                "type": "string",
                                "example": "runbook.pdf"
                              },
                              "content_type": {
                                "type": "string",
                                "nullable": true,
                                "example": "application/pdf"
                              },
                              "byte_size": {
                                "type": "integer",
                                "example": 30553
                              },
                              "is_image": {
                                "type": "boolean",
                                "example": false
                              },
                              "url": {
                                "type": "string",
                                "nullable": true,
                                "description": "Absolute URL for opening the attachment (null if the blob is unreadable). An IMAGE is served `Content-Disposition: inline` so it can be previewed in place; every other file — a PDF above all — is served `Content-Disposition: attachment`, because a client with no built-in PDF renderer can neither display an inline PDF nor hand it to a download, so the tap does nothing at all.",
                                "example": "https://officechat.workforce.mangoapps.com/rails/active_storage/blobs/redirect/x/runbook.pdf?disposition=attachment"
                              },
                              "download_url": {
                                "type": "string",
                                "nullable": true,
                                "description": "Absolute URL that ALWAYS responds `Content-Disposition: attachment` — the target for an explicit Download action, images included. Identical to `url` for every non-image attachment.",
                                "example": "https://officechat.workforce.mangoapps.com/rails/active_storage/blobs/redirect/x/runbook.pdf?disposition=attachment"
                              }
                            }
                          }
                        },
                        "parents": {
                          "type": "array",
                          "description": "The wiki's ancestor chain, ROOT FIRST and excluding the wiki itself — render it as breadcrumbs above the title. Empty for a top-level wiki. Ancestors the caller cannot view are DROPPED (so the chain may skip a level) rather than leaking the title of a page this endpoint would 404; a trashed ancestor truncates the chain there.",
                          "items": {
                            "type": "object",
                            "required": [
                              "id",
                              "title"
                            ],
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 4188
                              },
                              "title": {
                                "type": "string",
                                "example": "Employee Handbook"
                              }
                            }
                          },
                          "example": [
                            {
                              "id": 4188,
                              "title": "Employee Handbook"
                            },
                            {
                              "id": 4192,
                              "title": "Benefits"
                            },
                            {
                              "id": 4201,
                              "title": "Dental"
                            }
                          ]
                        },
                        "sub_wikis": {
                          "type": "array",
                          "description": "The nested tree of this wiki's sub-wikis — every published, caller-visible descendant, recursively. Each node is a card that itself carries a `sub_wikis` array (empty at the leaves). A descendant reachable only through a hidden (draft/restricted) parent does not appear. Same visibility as GET /wikis/{id}/sub_wikis, but that endpoint returns ONE level while this is the whole subtree.\n\n**Capped at 200 nodes**, cut BREADTH-FIRST so what you receive is the top of the outline — a coherent partial hierarchy, never a dangling branch: a node is present only if its parent is, so `sub_wikis` can always be rendered as a tree as-is. Check `sub_wikis_truncated` before treating it as complete, and fetch the missing levels one at a time with GET /wikis/{id}/sub_wikis.",
                          "items": {
                            "$ref": "#/components/schemas/WikiSubTreeNode"
                          }
                        },
                        "sub_wikis_total": {
                          "type": "integer",
                          "description": "How many nodes the whole subtree holds — every published, caller-visible descendant, counted before the 200-node cap. Equal to the number of nodes in `sub_wikis` when `sub_wikis_truncated` is false.",
                          "example": 6
                        },
                        "sub_wikis_truncated": {
                          "type": "boolean",
                          "description": "True when the cap cut the tree, i.e. `sub_wikis` holds fewer than `sub_wikis_total` nodes. **Additive** — a client that never reads it behaves exactly as before.",
                          "example": false
                        }
                      }
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Wikis app is not accessible to the caller (error code `access_denied`)."
          },
          "404": {
            "description": "No wiki with that id is visible to the caller — missing, restricted, another user's draft, or another tenant's (error code `not_found`)."
          }
        }
      },
      "delete": {
        "tags": [
          "Wikis"
        ],
        "summary": "Delete a wiki (soft-delete → Trash)",
        "description": "Soft-deletes a wiki — the native mirror of the web reader's delete action\n(`Apps::Wikis::PagesController#destroy` → `Wiki#discard!`). The page AND\nits entire sub-tree are moved to Trash in one transaction: they leave every\nlist/read immediately but stay recoverable from web Trash. This is NOT a\npermanent delete.\n\n* **Authorization — only users who may MANAGE the wiki** (the web\n  `can_manage_wiki?` gate): a business admin-or-above OR the wiki's\n  creator. A member who can merely VIEW the page gets `403`; a page the\n  caller cannot see at all returns `404`, never revealing it exists.\n* `sub_wikis_deleted` reports how many descendant pages were trashed with\n  it (the cascade).\n* Deleting an already-trashed page returns `404` (it has left the default\n  scope), so the call is safe to retry.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Wiki moved to trash",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "wiki_id",
                    "deleted",
                    "sub_wikis_deleted",
                    "message"
                  ],
                  "properties": {
                    "wiki_id": {
                      "type": "integer",
                      "description": "The id of the deleted wiki.",
                      "example": 2633
                    },
                    "deleted": {
                      "type": "boolean",
                      "description": "Always true on a 200 (the wiki was moved to Trash).",
                      "example": true
                    },
                    "sub_wikis_deleted": {
                      "type": "integer",
                      "description": "How many descendant sub-pages were trashed with it (cascade).",
                      "example": 2
                    },
                    "message": {
                      "type": "string",
                      "example": "Wiki moved to trash."
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller may view the wiki but is neither an admin-or-above nor its creator, OR the Wikis app is not accessible to them (error code `access_denied`)."
          },
          "404": {
            "description": "No wiki with that id is visible to the caller — missing, restricted, another user's draft, another tenant's, or already trashed (error code `not_found`)."
          }
        }
      }
    },
    "/alerts": {
      "get": {
        "tags": [
          "Alerts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List the caller's received alerts (+ pending approvals)",
        "description": "Mirrors the broadcasts list API. Returns the caller's own alert inbox —\nalerts that were SENT to them (they have an `AlertDelivery` row), in\nsent/completed status — plus a `pending_approvals` array of alerts\nawaiting the caller's approval.\n\nOrdering mirrors the web Dashboard \"Emergency Alerts\" tab — most recent first (created_at DESC). Supports the subfilters via\n`filter`.\n\n`meta.segment_counts` is NOT the size of each tab's list. It counts\nwhat still NEEDS THE CALLER'S RESPONSE — the red-badge semantics — and\nis independent of the active `filter`. Two of the four therefore differ\nfrom their tab by design: `acknowledge` counts unacknowledged rows\nwhile its tab lists every ack-required alert (deliberately decoupled,\nISS-20260713-662), and `all` counts alerts still owing a response while\nits list is the whole inbox, which additionally includes alerts the\ncaller AUTHORED. `safety_check_in` and `draft` do match their tabs.\nMeasured on a live tenant: all 24 vs 16, acknowledge 11 vs 10,\nsafety_check_in 7 vs 7, draft 0 vs 0. Render tab badges from these\ncounts only if you want \"needs my response\", not \"how many rows\".\n",
        "parameters": [
          {
            "name": "filter",
            "in": "query",
            "description": "Subfilter. Default `all`.\n  * `all`             — every received alert (most recent first)\n  * `acknowledge`     — `ack_required = true`\n  * `safety_check_in` — `safety_check_in_required = true`\n                        (also accepts `safety` / `safety check in`)\n  * `draft`           — the CALLER's OWN unsent alerts (authored +\n                        status 'draft'): plain drafts, ones awaiting\n                        approval, approved-pending-send, and rejected.\n                        These `alerts` items additionally carry\n                        `approval_state` + `approval_id` (and, when\n                        approved/rejected, `approval_decided_by`,\n                        `approval_decided_at`, `approval_notes`).\n(No `urgent` filter — the accountability invariant makes every alert\nurgent, so it would be identical to `all`.)\n",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "acknowledge",
                "safety_check_in",
                "draft"
              ],
              "default": "all"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number (default 1).",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Items per page (default 25, max 100).",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Alerts listed (received inbox, most recent first)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "alerts",
                    "pending_approvals",
                    "can_manage",
                    "meta"
                  ],
                  "properties": {
                    "alerts": {
                      "type": "array",
                      "description": "The filtered alert list. For `?filter=draft` these are the\ncaller's OWN unsent alerts and each item additionally carries\n`approval_state` (draft | pending_approval | approved | rejected)\n+ `approval_id` (the latest approval request id, null for a\nplain draft). Approved/rejected drafts also carry\n`approval_decided_by` (reviewer name), `approval_decided_at`\n(ISO-8601), and `approval_notes`. Other filters omit these fields.\n",
                      "items": {
                        "$ref": "#/components/schemas/AlertSummary"
                      }
                    },
                    "pending_approvals": {
                      "type": "array",
                      "description": "Alerts awaiting the CALLER's approval (pending Comms Hub\napproval requests whose current step targets the caller's\nrole; admins see all). Same item shape as `alerts`.\nIndependent of pagination/?filter=.\n",
                      "items": {
                        "$ref": "#/components/schemas/AlertSummary"
                      }
                    },
                    "can_manage": {
                      "type": "boolean",
                      "description": "Root-level capability flag (not per item): whether the caller\ncan manage emergency alerts (send / cancel / view tracking) —\nmanager+, mirroring the web authorize_alert_access gate.\n"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/AlertListMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "The Broadcasts & Alerts app is not enabled for this business"
          }
        }
      },
      "post": {
        "tags": [
          "Alerts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Create an alert — save as draft or publish (manager+)",
        "description": "Create an emergency alert. The client chooses the disposition via\n**`alert.status`**:\n- `\"publish\"` — the alert is **dispatched immediately** (status\n  `sending`, the channel fan-out is enqueued), mirroring the web\n  composer's **Send Now** — or routed through approval (see below).\n- **anything else** — `\"draft\"`, an **absent** status, or any\n  unrecognized value — the alert is **saved as a draft only**: NOT routed\n  through approval and NOT dispatched to recipients. Send it later via\n  `POST /alerts/{id}/send_now`, or discard it via `DELETE /alerts/{id}`.\n  The response carries `status: \"draft\"`.\n\n**Fail-safe default:** an alert is only ever sent when `alert.status` is\nexplicitly `\"publish\"`; every other case is a draft.\n\nRequires **manager or above** (the web `authorize_alert_access` gate for\nemergency alerts).\n\n**Approval (publish only):** if an ENFORCED approval workflow governs\nemergency alerts, a non-admin's *publish* is routed through approval\ninstead of dispatching — the alert is submitted for review and the\nresponse returns `status: \"pending_approval\"` (with `alert.status:\n\"draft\"`); an approver dispatches it later. Admins override approval and\ndispatch directly (same as the web Send-Now). A `\"draft\"` is never\nsubmitted for approval.\n\n**Targeting** is optional: supply any of `audience_id`,\n`notification_recipient_group_ids`, `alert.extra_user_ids`, or\n`alert.audience_criteria`. When none is supplied the alert targets\nEVERYONE in the business (the model's fallback), matching the web composer.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "alert"
                ],
                "properties": {
                  "alert": {
                    "type": "object",
                    "required": [
                      "title",
                      "body"
                    ],
                    "properties": {
                      "title": {
                        "type": "string"
                      },
                      "body": {
                        "type": "string"
                      },
                      "status": {
                        "type": "string",
                        "enum": [
                          "draft",
                          "publish"
                        ],
                        "description": "Disposition of the new alert. `\"publish\"` runs the send\nflow (immediate dispatch, or approval when an enforced\nworkflow governs it). Every other value — `\"draft\"`, an\nABSENT status, or anything unrecognized — saves it as a\ndraft only (NOT routed through approval, NOT dispatched;\nsend later via send_now, or discard via DELETE). Fail-safe:\nonly an explicit `\"publish\"` ever sends.\n"
                      },
                      "sms_body": {
                        "type": "string"
                      },
                      "action_url": {
                        "type": "string"
                      },
                      "urgent": {
                        "type": "boolean"
                      },
                      "ack_required": {
                        "type": "boolean"
                      },
                      "safety_check_in_required": {
                        "type": "boolean"
                      },
                      "audience_id": {
                        "type": "integer",
                        "nullable": true,
                        "description": "A saved CommsHub audience to send to."
                      },
                      "channels": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      "extra_user_ids": {
                        "type": "array",
                        "description": "Specific user ids to send to (stored as the alert's manual recipient list).",
                        "items": {
                          "type": "integer"
                        }
                      },
                      "audience_criteria": {
                        "type": "array",
                        "description": "Attribute-based audience filters, each a typed hash — e.g.\n`{ \"type\": \"role\", \"roles\": [\"member\"] }`,\n`{ \"type\": \"job_title\", \"titles\": [\"Area Manager\"] }`,\n`{ \"type\": \"department\", \"ids\": [1,2] }`,\n`{ \"type\": \"location\", \"ids\": [3] }`. Validated + business-\nscoped server-side.\n",
                        "items": {
                          "type": "object",
                          "additionalProperties": true
                        }
                      },
                      "media_signed_ids": {
                        "type": "array",
                        "description": "Attachments. Pre-upload each file via\n`POST /rails/active_storage/direct_uploads` (standard\nActiveStorage direct upload) and pass the resulting blob\n`signed_id`s here. They are attached to the alert's\n`media_files` (Drive) — the same attachments the web\ncomposer and the show API expose. Validated server-side\nagainst the alert media rules (photos/video only, per-file\nsize cap, max 4 files); any invalid reference fails the\nwhole create with 422 and nothing is persisted.\n",
                        "items": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "notification_recipient_group_ids": {
                    "type": "array",
                    "description": "Notification recipient group ids to send to (top-level, not\nunder `alert`). Business-scoped server-side.\n",
                    "items": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Alert created. When `alert.status: \"draft\"` the alert is **saved as a\ndraft** (`status: \"draft\"`) — not dispatched, not sent for approval.\nOtherwise it is **published**: normally **dispatched** immediately\n(`status: \"sending\"`); or, if an enforced approval workflow governs\nemergency alerts and the caller is NOT an admin (admins override), it\nis **submitted for approval** — the response carries\n`status: \"pending_approval\"` and `alert.status: \"draft\"`; an approver\ndispatches it later via `POST /approvals/{id}/approve`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "alert",
                    "status"
                  ],
                  "properties": {
                    "alert": {
                      "$ref": "#/components/schemas/AlertSummary"
                    },
                    "attachments": {
                      "type": "array",
                      "description": "The attachments that were attached to the alert (from media_signed_ids).",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "filename": {
                            "type": "string"
                          },
                          "content_type": {
                            "type": "string"
                          },
                          "byte_size": {
                            "type": "integer"
                          },
                          "url": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "draft",
                        "sending",
                        "pending_approval"
                      ],
                      "description": "`draft` (saved only), `sending` (dispatched), or\n`pending_approval` (routed to approval).\n"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Manager access required, or app not enabled"
          },
          "422": {
            "description": "Validation failed, or approval was required but could not be submitted (`approval_required`)"
          }
        }
      }
    },
    "/alerts/my": {
      "get": {
        "tags": [
          "Alerts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "The caller's personal received-alert inbox",
        "description": "The API twin of the web My Alerts page (`/apps/safety-hub/alerts/my`): a\nPURE recipient inbox. Returns ONLY alerts that were SENT to the caller\n(they hold an `AlertDelivery` row), in a delivered state\n(sending/sent/completed/cancelled), most recent first.\n\nDeliberately different from `GET /alerts` (the mobile list): this endpoint\nhas NO drafts, never surfaces alerts the caller AUTHORED but was not a\nrecipient of, and carries NO manager envelope — there is no\n`pending_approvals`, `can_manage`, or `can_dispatch`. Open to every member,\nexactly like the web page.\n\n`meta.segment_counts` counts what still NEEDS THE CALLER'S RESPONSE\n(all / acknowledge / safety_check_in) over the received inbox, independent\nof the active `filter`. There is no `draft` segment.\n",
        "parameters": [
          {
            "name": "filter",
            "in": "query",
            "description": "Subfilter. Default `all`.\n  * `all`             — every received alert (most recent first)\n  * `acknowledge`     — `ack_required = true`\n  * `safety_check_in` — `safety_check_in_required = true`\n                        (also accepts `safety` / `safety check in`)\n`draft` is not valid here (this feed has no drafts) and is coerced to\n`all`; the applied value is echoed as `meta.applied_filter`.\n",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "acknowledge",
                "safety_check_in"
              ],
              "default": "all"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number (default 1).",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Items per page (default 25, max 100).",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's received alerts, most recent first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "alerts",
                    "meta"
                  ],
                  "properties": {
                    "alerts": {
                      "type": "array",
                      "description": "Alerts sent to the caller, each with the caller's own response state.",
                      "items": {
                        "$ref": "#/components/schemas/AlertSummary"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page",
                        "applied_filter",
                        "segment_counts"
                      ],
                      "properties": {
                        "total_count": {
                          "type": "integer"
                        },
                        "total_pages": {
                          "type": "integer"
                        },
                        "current_page": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        },
                        "applied_filter": {
                          "type": "string",
                          "enum": [
                            "all",
                            "acknowledge",
                            "safety_check_in"
                          ],
                          "description": "The filter actually applied (a `draft` request is coerced to `all`)."
                        },
                        "segment_counts": {
                          "type": "object",
                          "description": "\"Needs my response\" counts over the received inbox, independent of `filter`. No `draft` segment (this feed has no drafts).\n",
                          "required": [
                            "all",
                            "acknowledge",
                            "safety_check_in"
                          ],
                          "properties": {
                            "all": {
                              "type": "integer"
                            },
                            "acknowledge": {
                              "type": "integer"
                            },
                            "safety_check_in": {
                              "type": "integer"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Missing the read:broadcasts scope, or the app/module is not enabled"
          }
        }
      }
    },
    "/alerts/team": {
      "get": {
        "tags": [
          "Alerts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Team Alerts — the manager register of every alert in the business",
        "description": "The API twin of the web Team Alerts page (`/apps/safety-hub/alerts`): the\nmanager/admin ops register of EVERY emergency alert in the business.\n\nDeliberately different from `GET /alerts` and `GET /alerts/my`, which are\nthe caller's RECEIVED inbox. This is the tenant-wide management list, so\nit is MANAGER+ gated (a `manager_or_above?` user or the Safety Hub app\nadmin — the same tier as the web `authorize_alert_access`) and carries the\nread token ceiling: a `read:own_broadcasts`-only token is refused (403).\n\nEvery alert in the business, most recent first, filterable by lifecycle\n`status` and title/body `q`, paginated. Each row carries the standard\nalert shape plus a `response_summary` (delivery progress), and `meta`\ncarries `status_counts` (per-tab totals) and the root-level `can_manage` /\n`can_dispatch` capability flags.\n",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "Filter by lifecycle status. Default `all` (no filter).\n  * `all`       — every status\n  * `draft`     — composing, not yet sent\n  * `sending`   — dispatch in progress (a transient state)\n  * `sent`      — all per-recipient jobs enqueued\n  * `completed` — every delivery terminal\n  * `cancelled` — aborted\nAn unrecognized value is treated as `all`; the applied value is echoed\nas `meta.applied_status`.\n",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "draft",
                "sending",
                "sent",
                "completed",
                "cancelled"
              ],
              "default": "all"
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Title/body search (tokenized; matches on word, not order).",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number (default 1).",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Items per page (default 25, max 100).",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Every alert in the business, most recent first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "alerts",
                    "can_manage",
                    "can_dispatch",
                    "meta"
                  ],
                  "properties": {
                    "alerts": {
                      "type": "array",
                      "description": "Every alert in the business (for this page), each with a delivery-progress summary.",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/AlertSummary"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "response_summary": {
                                "type": "object",
                                "description": "Delivery progress for the alert. `responded` / `outstanding` are response-mode aware: for a safety-check-in alert `responded` = safe + needs_help; for an acknowledge alert `responded` = acknowledged; for a plain urgent alert both are null (no response is owed).\n",
                                "properties": {
                                  "recipients": {
                                    "type": "integer"
                                  },
                                  "acknowledged": {
                                    "type": "integer"
                                  },
                                  "safe": {
                                    "type": "integer"
                                  },
                                  "needs_help": {
                                    "type": "integer"
                                  },
                                  "responded": {
                                    "type": "integer",
                                    "nullable": true
                                  },
                                  "outstanding": {
                                    "type": "integer",
                                    "nullable": true
                                  }
                                }
                              }
                            }
                          }
                        ]
                      }
                    },
                    "can_manage": {
                      "type": "boolean",
                      "description": "Whether the caller can manage emergency alerts (compose/stage/cancel/tracking)."
                    },
                    "can_dispatch": {
                      "type": "boolean",
                      "description": "Whether the caller may DISPATCH an alert (the narrower per-tenant policy)."
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page",
                        "applied_status",
                        "status_counts"
                      ],
                      "properties": {
                        "total_count": {
                          "type": "integer"
                        },
                        "total_pages": {
                          "type": "integer"
                        },
                        "current_page": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        },
                        "applied_status": {
                          "type": "string",
                          "enum": [
                            "all",
                            "draft",
                            "sending",
                            "sent",
                            "completed",
                            "cancelled"
                          ],
                          "description": "The status actually applied (an unrecognized request is coerced to `all`)."
                        },
                        "status_counts": {
                          "type": "object",
                          "description": "Per-tab totals over the whole business + search base, INDEPENDENT of the active `status` filter, so a filter-row badge always shows its true total. `all` is the sum.\n",
                          "required": [
                            "all",
                            "draft",
                            "sending",
                            "sent",
                            "completed",
                            "cancelled"
                          ],
                          "properties": {
                            "all": {
                              "type": "integer"
                            },
                            "draft": {
                              "type": "integer"
                            },
                            "sending": {
                              "type": "integer"
                            },
                            "sent": {
                              "type": "integer"
                            },
                            "completed": {
                              "type": "integer"
                            },
                            "cancelled": {
                              "type": "integer"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Not a manager, or the token is scoped to own alerts (needs read:broadcasts), or the app/module is not enabled"
          }
        }
      }
    },
    "/alerts/templates": {
      "get": {
        "tags": [
          "Alerts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List alert templates for the composer's \"Start from a template\" picker",
        "description": "Powers the alert composer's \"Start from a template\" picker (opened from\nthe alert list `+`). Returns the SAME two groups the web composer offers:\n  * `your_templates`   — this business's saved alert templates (ordered)\n  * `common_scenarios` — the platform-curated common scenarios\n                         (system templates, shared across businesses),\n                         so an author can start from a pre-approved\n                         emergency scenario.\nEach entry carries the composition fields the client pre-fills the\nnew-alert form with (title / body / sms_body / channels + the urgency /\nack / safety toggles). Requires **manager or above** — the same gate as\ncreating an alert (`authorize_alert_access` → `manager_or_above?`), so\nanyone who can create an alert can fetch the templates to start from.\n",
        "responses": {
          "200": {
            "description": "Templates listed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "your_templates",
                    "common_scenarios",
                    "meta"
                  ],
                  "properties": {
                    "your_templates": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AlertTemplate"
                      }
                    },
                    "common_scenarios": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AlertTemplate"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "your_templates_count",
                        "common_scenarios_count"
                      ],
                      "properties": {
                        "your_templates_count": {
                          "type": "integer"
                        },
                        "common_scenarios_count": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Manager access required, or the app is not enabled"
          }
        }
      }
    },
    "/alerts/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Alert ID",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Alerts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get an alert",
        "description": "Returns the full alert plus its file `attachments` (the alert's\nmedia_files — photos/videos uploaded with the alert, the same Drive\nattachments the web alert detail renders).\n\nFor a DRAFT (unpublished) alert, also carries the approval-pipeline\nfields (`approval_state`, `approval_id`, `approval_decided_by`,\n`approval_decided_at`, `approval_notes`) — the same shape the\n`?filter=draft` list returns per item. These reflect the alert's latest\nComms Hub approval request. They are `null` once the alert is published\n(sent/scheduled), for a draft that never entered the approval pipeline,\nor for a caller who may not see the alert's internal review trail (a\nnon-author, non-privileged viewer — the same gate as `can_view_tracking`).\n",
        "responses": {
          "200": {
            "description": "Alert found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "alert"
                  ],
                  "properties": {
                    "alert": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/AlertSummary"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "acknowledged": {
                              "type": "boolean",
                              "description": "Whether the calling user has acknowledged this alert. Forced to true when the caller is the alert's author and is NOT in its audience (no delivery / not a recipient) — they have nothing to acknowledge, so the detail screen doesn't strand an unacknowledged state they can't clear.\n"
                            },
                            "can_manage": {
                              "type": "boolean",
                              "description": "True if and only if the caller may view THIS alert's per-recipient tracking details (acknowledgement / check-in rosters, recipient PII) — the alert's author, a broadcast app-admin, or an admin+. Matches the gate on GET /alerts/{id}/acknowledgements, /check_ins, and /recipients, so the client can show the \"Tracking Details\" affordance only when those calls will succeed.\n"
                            },
                            "attachments": {
                              "type": "array",
                              "description": "The alert's file attachments (DriveItem media).",
                              "items": {
                                "type": "object",
                                "required": [
                                  "id",
                                  "filename"
                                ],
                                "properties": {
                                  "id": {
                                    "type": "integer"
                                  },
                                  "filename": {
                                    "type": "string"
                                  },
                                  "content_type": {
                                    "type": "string",
                                    "nullable": true
                                  },
                                  "byte_size": {
                                    "type": "integer"
                                  },
                                  "url": {
                                    "type": "string",
                                    "description": "ActiveStorage blob URL (attachment disposition)."
                                  }
                                }
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "404": {
            "description": "Alert not found"
          }
        }
      },
      "patch": {
        "tags": [
          "Alerts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Edit a draft alert (manager+)",
        "description": "Edit a **DRAFT** alert. The body is the **same shape as `POST /alerts`**\n(the `alert[...]` fields + `notification_recipient_group_ids` +\n`alert[media_signed_ids]` + the `alert[status]` disposition). PATCH\nsemantics: only the fields you send are changed — an omitted field is\nleft as-is, and audience targeting is replaced only when you send\ntargeting params. `alert[media_signed_ids]` are ADDED to the draft\n(already-attached media is untouched).\n\nLike create, `alert[status]` chooses the disposition: `\"publish\"` sends\nthe alert now (dispatch, or approval when an enforced workflow governs a\nnon-admin) — the response then carries `status: \"sending\"` /\n`\"pending_approval\"`; any other value (incl. absent) keeps it a\n**draft**. So a client can edit-and-publish in one call.\n\nConstraints (mirror the web AlertsController#update): only a **draft** is\neditable (`422 not_a_draft` otherwise), and a draft with a **pending\napproval request** must have it withdrawn first (`422 approval_pending`).\nRequires **manager or above**.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "alert"
                ],
                "properties": {
                  "alert": {
                    "type": "object",
                    "description": "Same permitted fields as `POST /alerts`. All are optional on\nedit — send only what changes.\n",
                    "properties": {
                      "status": {
                        "type": "string",
                        "enum": [
                          "draft",
                          "publish"
                        ],
                        "description": "Disposition — `\"publish\"` sends; anything else keeps it a draft."
                      },
                      "title": {
                        "type": "string"
                      },
                      "body": {
                        "type": "string"
                      },
                      "sms_body": {
                        "type": "string"
                      },
                      "action_url": {
                        "type": "string"
                      },
                      "urgent": {
                        "type": "boolean"
                      },
                      "ack_required": {
                        "type": "boolean"
                      },
                      "safety_check_in_required": {
                        "type": "boolean"
                      },
                      "audience_id": {
                        "type": "integer",
                        "nullable": true
                      },
                      "channels": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      "extra_user_ids": {
                        "type": "array",
                        "items": {
                          "type": "integer"
                        },
                        "description": "Specific user ids (replaces the draft's manual recipient list when sent)."
                      },
                      "audience_criteria": {
                        "type": "array",
                        "description": "Attribute-based audience filters (same typed hashes as create).",
                        "items": {
                          "type": "object",
                          "additionalProperties": true
                        }
                      },
                      "media_signed_ids": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        },
                        "description": "Newly pre-uploaded ActiveStorage blob signed_ids to ADD to the draft."
                      }
                    }
                  },
                  "notification_recipient_group_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "Notification recipient group ids (top-level; replaces the draft's groups when sent)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Draft edited. `status: \"draft\"` when kept as a draft; `\"sending\"` when\npublished (dispatched); `\"pending_approval\"` when published into an\nenforced approval workflow (with `alert.status: \"draft\"`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "alert",
                    "status"
                  ],
                  "properties": {
                    "alert": {
                      "$ref": "#/components/schemas/AlertSummary"
                    },
                    "attachments": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "filename": {
                            "type": "string"
                          },
                          "content_type": {
                            "type": "string"
                          },
                          "byte_size": {
                            "type": "integer"
                          },
                          "url": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "draft",
                        "sending",
                        "pending_approval"
                      ]
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Manager access required to manage emergency alerts"
          },
          "404": {
            "description": "Alert not found"
          },
          "422": {
            "description": "The alert is not a draft (`not_a_draft`), has a pending approval\nrequest that must be withdrawn first (`approval_pending`), failed\nvalidation (`validation_failed`), had an invalid attachment\n(`invalid_attachments`), or couldn't be dispatched (`dispatch_failed`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "not_a_draft",
                            "approval_pending",
                            "validation_failed",
                            "invalid_attachments",
                            "approval_required",
                            "dispatch_failed"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Alerts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Discard a draft alert (manager+)",
        "description": "Permanently deletes a DRAFT alert — mirrors the web \"Discard draft\"\naction (AlertsController#destroy). Only a draft can be discarded: once an\nalert has been sent (or is sending/cancelled) it is part of the delivery\nrecord and is retained. A draft awaiting approval must have its approval\nrequest withdrawn first, so a pending CommsHub::ApprovalRequest is never\norphaned. Manager+ gated (same tier as create).\n",
        "responses": {
          "200": {
            "description": "Draft discarded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "alert_id",
                    "status"
                  ],
                  "properties": {
                    "alert_id": {
                      "type": "integer"
                    },
                    "status": {
                      "type": "string",
                      "example": "discarded"
                    },
                    "message": {
                      "type": "string",
                      "example": "Draft discarded."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Manager access required"
          },
          "404": {
            "description": "Alert not found"
          },
          "422": {
            "description": "The alert is not a draft (error code `not_a_draft`), is awaiting approval and must be withdrawn first (`approval_pending`), or could not be discarded (`discard_failed`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "not_a_draft",
                            "approval_pending",
                            "discard_failed"
                          ]
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/alerts/{id}/send_now": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Alert ID",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Alerts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Dispatch a draft alert now (manager+)",
        "description": "Flips a draft alert to `sending` and enqueues the dispatch fan-out. Manager-and-above only. When an enforced approval workflow governs emergency alerts, a non-admin (manager) can dispatch ONLY once the alert's approval request is `approved` — pending, rejected, withdrawn, and never-submitted alerts are blocked with `422 approval_required`. Admins always bypass the workflow. A non-enforced or absent workflow never blocks.\n",
        "responses": {
          "200": {
            "description": "Alert enqueued"
          },
          "403": {
            "description": "Manager access required, or the alert is already dispatched"
          },
          "404": {
            "description": "Alert not found"
          },
          "422": {
            "description": "Approval is required before this alert can be dispatched (error code `approval_required`) — the alert is pending, rejected, or was never submitted under an enforced workflow.\n"
          }
        }
      }
    },
    "/alerts/{id}/cancel": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Alert ID",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Alerts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Cancel a draft/sending alert (admin only)",
        "responses": {
          "200": {
            "description": "Alert cancelled"
          },
          "403": {
            "description": "Admin access required, or alert cannot be cancelled"
          },
          "404": {
            "description": "Alert not found"
          }
        }
      }
    },
    "/alerts/{id}/submit_for_approval": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Alert ID",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Alerts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Submit a draft alert for approval (manager+)",
        "description": "Submits a DRAFT alert for review against the governing enforced approval workflow for emergency alerts — the API mirror of the web AlertsController#submit_for_approval, reusing the shared CommsHub::ApprovalEnforceable#submit_source_for_approval. Manager+ (same gate as create). The inverse of withdraw_approval. Reviewers are notified.\n",
        "responses": {
          "200": {
            "description": "Alert submitted for approval (a pending request now exists; the alert stays draft)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "alert_id": {
                      "type": "integer"
                    },
                    "status": {
                      "type": "string",
                      "description": "Always \"pending_approval\" on success."
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Manager access required (or the Broadcasts & Alerts app is not enabled)"
          },
          "404": {
            "description": "Alert not found"
          },
          "422": {
            "description": "Cannot submit — the alert is not a draft (not_a_draft), no workflow governs emergency alerts (no_workflow), a request is already pending (already_pending), or the submit otherwise failed (submit_failed / no_item).\n"
          }
        }
      }
    },
    "/alerts/{id}/withdraw_approval": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Alert ID",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Alerts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Withdraw a pending approval request (manager+)",
        "description": "Pulls back the alert's pending Comms Hub approval request so the author can edit and resubmit — mirrors the web AlertsController#withdraw_approval. Manager+ at the controller (same gate as create); the model additionally allows only the ORIGINAL REQUESTER or an admin to actually withdraw, so a manager who did not submit it (and is not an admin) gets 422.\n",
        "responses": {
          "200": {
            "description": "Approval request withdrawn (the alert returns to draft, editable)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "alert_id": {
                      "type": "integer"
                    },
                    "status": {
                      "type": "string",
                      "description": "The alert's status after withdrawal (draft)."
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Manager access required (or the Broadcasts & Alerts app is not enabled)"
          },
          "404": {
            "description": "Alert not found, or there is no pending approval request to withdraw"
          },
          "422": {
            "description": "Could not withdraw (e.g. the caller is neither the original requester nor an admin)"
          }
        }
      }
    },
    "/alerts/{id}/check_in": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Alert ID",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Alerts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Record a safety check-in response (recipient)",
        "description": "Records the caller's safety check-in response, mirroring the web\nAlertsController#check_in_submit. The caller must be a **recipient** of\nthe alert (have an `AlertDelivery` row) — otherwise `403`.\n\nIdempotent upsert: a re-submit **updates** the caller's existing response,\nso a recipient can change it (e.g. from `needs_help` to `safe`). The\n`note` (situation details) is captured on the `needs_help` path and\n**cleared** when the recipient updates back to `safe`.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "status"
                ],
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "safe",
                      "needs_help"
                    ]
                  },
                  "note": {
                    "type": "string",
                    "nullable": true,
                    "description": "Optional situation details. Stored only when `status` is\n`needs_help`; ignored / cleared when `status` is `safe`.\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Check-in recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "alert_id",
                    "status",
                    "message"
                  ],
                  "properties": {
                    "alert_id": {
                      "type": "integer"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "safe",
                        "needs_help"
                      ]
                    },
                    "note": {
                      "type": "string",
                      "nullable": true,
                      "description": "The stored situation note (null for a safe response)."
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Not a recipient of this alert"
          },
          "404": {
            "description": "Alert not found"
          },
          "422": {
            "description": "Invalid status"
          }
        }
      }
    },
    "/alerts/{id}/acknowledge": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Alert ID",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Alerts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Acknowledge an ack-required alert (recipient)",
        "description": "Records the caller's explicit read-confirmation for an acknowledge-type\nalert, mirroring the web AlertsController#acknowledge. **Recipient-gated**\n— the caller must be a recipient of the alert (have an `AlertDelivery`\nrow); otherwise `403`. **Idempotent**: re-acknowledging is a no-op success\nthat preserves the original `acknowledged_at`.\n",
        "responses": {
          "200": {
            "description": "Acknowledged",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "alert_id",
                    "acknowledged"
                  ],
                  "properties": {
                    "alert_id": {
                      "type": "integer"
                    },
                    "acknowledged": {
                      "type": "boolean"
                    },
                    "acknowledged_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Not a recipient of this alert (`not_a_recipient`)"
          },
          "404": {
            "description": "Alert not found"
          }
        }
      }
    },
    "/alerts/{id}/check_ins": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Alert ID",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Alerts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Safety check-in tracking roster (Tracking Details)",
        "description": "Powers the web **Tracking Details** screen for a safety-check-in alert:\nthe alert's recipients (everyone it was delivered to) with their safety\nresponse, plus the safe / needs-help / unresponded counts.\n\n**Manager or above** — mirrors the web alert tracking page authorization\n(`authorize_alert_access` → `manager_or_above?`), same as the other\ntracking rosters. Only meaningful for an alert with\n`safety_check_in_required = true` (others return `422`).\n\nEach row carries the responder's `status` (`safe` / `needs_help` /\n`unresponded`), `responded_at`, and `note` (the situation details a\n\"needs help\" responder left). The recipient set is scoped to the\nbusiness's users; rows are ordered by responder name (case-insensitive,\nwith user id as a unique tiebreaker so paging cannot repeat or skip a\nrecipient) — 20/page by default. Use `status` for the prioritized tabs.\n",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "Roster filter. Default `all`.\n  * `all`         — every recipient, each annotated with their status\n  * `safe`        — recipients who marked themselves safe\n  * `needs_help`  — recipients who responded \"needs help\"\n  * `unresponded` — recipients who have not responded yet\n",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "safe",
                "needs_help",
                "unresponded"
              ],
              "default": "all"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number (default 1).",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Items per page (default 20, max 100).",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Recipients listed with their safety response, plus counts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "recipients",
                    "meta"
                  ],
                  "properties": {
                    "recipients": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "status"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string",
                            "nullable": true
                          },
                          "avatar_url": {
                            "type": "string",
                            "nullable": true,
                            "description": "Absolute avatar URL (profile photo variant, else a ui-avatars fallback)."
                          },
                          "title": {
                            "type": "string",
                            "nullable": true,
                            "description": "Recipient's job title (row subtitle)."
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "safe",
                              "needs_help",
                              "unresponded"
                            ]
                          },
                          "responded_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "description": "When the recipient responded (null when unresponded)."
                          },
                          "note": {
                            "type": "string",
                            "nullable": true,
                            "description": "Situation details left by a \"needs help\" responder (null otherwise)."
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "status",
                        "total_recipients_count",
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ],
                      "properties": {
                        "status": {
                          "type": "string",
                          "description": "The resolved roster filter (all | safe | needs_help | unresponded)."
                        },
                        "total_recipients_count": {
                          "type": "integer",
                          "description": "All recipients the alert was delivered to."
                        },
                        "safe_count": {
                          "type": "integer",
                          "description": "Recipients who marked themselves safe. Included only on the first page (page == 1)."
                        },
                        "need_help_count": {
                          "type": "integer",
                          "description": "Recipients who responded \"needs help\". Included only on the first page (page == 1)."
                        },
                        "unresponded_count": {
                          "type": "integer",
                          "description": "Recipients who have not responded yet. Included only on the first page (page == 1)."
                        },
                        "total_count": {
                          "type": "integer",
                          "description": "Items matching the active status filter (this list's size)."
                        },
                        "total_pages": {
                          "type": "integer"
                        },
                        "current_page": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Manager access required"
          },
          "404": {
            "description": "Alert not found"
          },
          "422": {
            "description": "Alert does not require a safety check-in (`not_safety_check_in`)"
          }
        }
      }
    },
    "/alerts/{id}/acknowledgements": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Alert ID",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Alerts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Acknowledgement tracking roster (Tracking Details)",
        "description": "Powers the web **Tracking Details** screen for an ACK-REQUIRED alert:\nthe alert's recipients (everyone it was delivered to) filtered by whether\nthey acknowledged, plus the ack / non-ack counts.\n\n**Manager or above** — mirrors the web alert tracking page authorization\n(`authorize_alert_access` → `manager_or_above?`). Only meaningful for an\nalert with `ack_required = true` (others return `422`).\n\nEach acked row carries the `acknowledged_at` date the screen shows\n(null on the `not_acked` tab). The recipient set is scoped to the\nbusiness's users. The `acked` tab is ordered by acknowledgement time\n(newest first) with user id as a unique tiebreaker; the `not_acked` tab\nis ordered by user id. 20/page by default.\n",
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "description": "Roster filter. Default `acked`.\n  * `acked`     — recipients who HAVE acknowledged (with acknowledged_at)\n  * `not_acked` — recipients who have NOT acknowledged\n",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "acked",
                "not_acked"
              ],
              "default": "acked"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number (default 1).",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Items per page (default 20, max 100).",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Recipients listed with acknowledgement state, plus counts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "recipients",
                    "meta"
                  ],
                  "properties": {
                    "recipients": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string",
                            "nullable": true
                          },
                          "avatar_url": {
                            "type": "string",
                            "nullable": true,
                            "description": "Absolute avatar URL (profile photo variant, else a ui-avatars fallback)."
                          },
                          "title": {
                            "type": "string",
                            "nullable": true,
                            "description": "Recipient's job title (row subtitle)."
                          },
                          "acknowledged_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "description": "When the recipient acknowledged (null on the not_acked tab)."
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "type",
                        "total_recipients_count",
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ],
                      "properties": {
                        "type": {
                          "type": "string",
                          "description": "The resolved roster filter (acked | not_acked)."
                        },
                        "total_recipients_count": {
                          "type": "integer",
                          "description": "All recipients the alert was delivered to."
                        },
                        "ack_count": {
                          "type": "integer",
                          "description": "Recipients who have acknowledged. Included only on the first page (page == 1)."
                        },
                        "non_acked_count": {
                          "type": "integer",
                          "description": "Recipients who have not acknowledged. Included only on the first page (page == 1)."
                        },
                        "total_count": {
                          "type": "integer",
                          "description": "Items matching the active type filter (this list's size)."
                        },
                        "total_pages": {
                          "type": "integer"
                        },
                        "current_page": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Manager access required"
          },
          "404": {
            "description": "Alert not found"
          },
          "422": {
            "description": "Alert does not require acknowledgment (`not_ack_required`)"
          }
        }
      }
    },
    "/alerts/{id}/recipients": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Alert ID",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Alerts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Alert delivery roster (Tracking Details — Recipients)",
        "description": "Powers the **Recipients** section of the web \"Tracking Details\" screen —\nthe only tracking section present for a plain urgent alert (neither\nacknowledge- nor safety-check-in-required), and also available for the\nother alert types.\n\nReturns every recipient (everyone the alert was delivered to) with the\n**channels they were reached on** (in app / email / push / sms / voice,\neach with delivery status + time). **Manager+ only** (mirrors the web\nalerts#show authorize_alert_access gate).\n\nThe recipient set is scoped to the business's users; rows are ordered by\nuser id for stable pagination (20/page by default). `meta.channel_counts`\nis the per-channel reach (distinct recipients reached on each channel) —\nthe per-channel summary the screen shows. It is a roster-wide aggregate\nand is returned **only on page 1** (the client renders it once in the\nheader); it is omitted on subsequent pages.\n",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number (default 1).",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Items per page (default 20, max 100).",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Recipients listed with the channels they were reached on",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "recipients",
                    "meta"
                  ],
                  "properties": {
                    "recipients": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "channels"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string",
                            "nullable": true
                          },
                          "avatar_url": {
                            "type": "string",
                            "nullable": true,
                            "description": "Absolute avatar URL (profile photo variant, else a ui-avatars fallback)."
                          },
                          "title": {
                            "type": "string",
                            "nullable": true,
                            "description": "Recipient's job title (row subtitle)."
                          },
                          "email": {
                            "type": "string",
                            "nullable": true,
                            "description": "Recipient's email (row subtitle)."
                          },
                          "channels": {
                            "type": "array",
                            "description": "The channels this recipient was reached on.",
                            "items": {
                              "type": "object",
                              "required": [
                                "channel"
                              ],
                              "properties": {
                                "channel": {
                                  "type": "string",
                                  "description": "Delivery channel (e.g. in_app, email, push, sms, voice)."
                                },
                                "status": {
                                  "type": "string",
                                  "nullable": true,
                                  "description": "Per-channel delivery status (e.g. sent, failed, skipped)."
                                },
                                "delivered_at": {
                                  "type": "string",
                                  "format": "date-time",
                                  "nullable": true
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "total_recipients_count",
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ],
                      "properties": {
                        "total_recipients_count": {
                          "type": "integer",
                          "description": "All recipients the alert was delivered to."
                        },
                        "channel_counts": {
                          "type": "object",
                          "description": "Distinct recipients reached per channel, keyed by channel name.\nReturned **only on page 1** (roster-wide aggregate); omitted on\nsubsequent pages.\n",
                          "additionalProperties": {
                            "type": "integer"
                          }
                        },
                        "total_count": {
                          "type": "integer"
                        },
                        "total_pages": {
                          "type": "integer"
                        },
                        "current_page": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Manager access required"
          },
          "404": {
            "description": "Alert not found"
          }
        }
      }
    },
    "/tinytake/captures": {
      "get": {
        "tags": [
          "TinyTake"
        ],
        "summary": "List captures",
        "description": "Retrieve a paginated list of screen captures and video recordings.\n\nUse this endpoint to:\n- View my screen captures\n- List recorded videos\n- Get capture history\n- Find screenshots by tags\n- Browse my recordings\n- See all TinyTake files\n\nSupports filtering by type, tags, and date range with pagination.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "description": "Filter by capture type",
            "schema": {
              "type": "string",
              "enum": [
                "image",
                "video",
                "all"
              ],
              "default": "all"
            }
          },
          {
            "name": "tags",
            "in": "query",
            "description": "Filter by tags (comma-separated)",
            "schema": {
              "type": "string"
            },
            "example": "meeting,presentation"
          },
          {
            "name": "folder_id",
            "in": "query",
            "description": "Filter by folder",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "created_after",
            "in": "query",
            "description": "Filter captures created after this date",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "created_before",
            "in": "query",
            "description": "Filter captures created before this date",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "sort_by",
            "in": "query",
            "description": "Field to sort by",
            "schema": {
              "type": "string",
              "enum": [
                "created_at",
                "updated_at",
                "name",
                "size"
              ],
              "default": "created_at"
            }
          },
          {
            "name": "sort_order",
            "in": "query",
            "description": "Sort direction",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Results per page (max 100)",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of captures retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "count": {
                      "type": "integer",
                      "example": 25
                    },
                    "page": {
                      "type": "integer",
                      "example": 1
                    },
                    "per_page": {
                      "type": "integer",
                      "example": 50
                    },
                    "total_pages": {
                      "type": "integer",
                      "example": 1
                    },
                    "captures": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TinyTakeCapture"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Upload capture",
        "description": "Upload a new screen capture or video recording.\n\nUse this endpoint to:\n- Upload screenshot\n- Save screen recording\n- Upload video capture\n- Save image to TinyTake\n- Store screen capture\n\nSupports PNG, JPG, GIF images and MP4, WebM videos.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The image or video file to upload"
                  },
                  "name": {
                    "type": "string",
                    "description": "Custom name for the capture (defaults to filename)",
                    "example": "Meeting Notes 2024-01-15"
                  },
                  "folder_id": {
                    "type": "string",
                    "description": "Target folder ID (defaults to root)"
                  },
                  "tags": {
                    "type": "string",
                    "description": "Comma-separated tags",
                    "example": "meeting,notes,q1"
                  },
                  "visibility": {
                    "type": "string",
                    "enum": [
                      "private",
                      "shared",
                      "public"
                    ],
                    "default": "private",
                    "description": "Capture visibility level"
                  },
                  "generate_thumbnail": {
                    "type": "boolean",
                    "default": true,
                    "description": "Generate thumbnail for the capture"
                  },
                  "transcode_video": {
                    "type": "boolean",
                    "default": false,
                    "description": "Transcode video for web playback"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Capture uploaded successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Capture uploaded successfully"
                    },
                    "capture": {
                      "$ref": "#/components/schemas/TinyTakeCapture"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid file or parameters",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "413": {
            "description": "File too large",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "507": {
            "description": "Insufficient storage quota",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/tinytake/captures/search": {
      "get": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Search captures",
        "description": "Full-text search across captures with advanced filtering.\n\nUse this endpoint to:\n- Search captures by name\n- Find captures by content (OCR text)\n- Search by tags\n- Find captures by date range\n- Advanced search with multiple filters\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "query",
            "in": "query",
            "description": "Search term (searches name, tags, OCR text)",
            "schema": {
              "type": "string"
            },
            "example": "meeting notes"
          },
          {
            "name": "type",
            "in": "query",
            "description": "Filter by capture type",
            "schema": {
              "type": "string",
              "enum": [
                "image",
                "video",
                "all"
              ]
            }
          },
          {
            "name": "tags",
            "in": "query",
            "description": "Filter by tags (comma-separated, AND logic)",
            "schema": {
              "type": "string"
            },
            "example": "meeting,important"
          },
          {
            "name": "folder_id",
            "in": "query",
            "description": "Filter by folder",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "visibility",
            "in": "query",
            "description": "Filter by visibility",
            "schema": {
              "type": "string",
              "enum": [
                "private",
                "shared",
                "public"
              ]
            }
          },
          {
            "name": "has_annotations",
            "in": "query",
            "description": "Filter by annotation presence",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "created_after",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "created_before",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "min_size",
            "in": "query",
            "description": "Minimum file size in bytes",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "max_size",
            "in": "query",
            "description": "Maximum file size in bytes",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "sort_by",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "relevance",
                "created_at",
                "updated_at",
                "name",
                "size"
              ],
              "default": "relevance"
            }
          },
          {
            "name": "sort_order",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Search results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "query": {
                      "type": "string"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "page": {
                      "type": "integer"
                    },
                    "per_page": {
                      "type": "integer"
                    },
                    "total_pages": {
                      "type": "integer"
                    },
                    "captures": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/TinyTakeCapture"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "relevance_score": {
                                "type": "number",
                                "description": "Search relevance score (0-1)"
                              },
                              "matched_text": {
                                "type": "string",
                                "description": "Snippet of matched text with highlights"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/tinytake/captures/recent": {
      "get": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Get recent captures",
        "description": "Retrieve recently viewed or edited captures for quick access.\n\nUse this endpoint to:\n- Get recently viewed captures\n- Quick access to recent files\n- Continue where you left off\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Number of recent captures to return",
            "schema": {
              "type": "integer",
              "default": 10,
              "maximum": 50
            }
          },
          {
            "name": "activity_type",
            "in": "query",
            "description": "Type of recent activity",
            "schema": {
              "type": "string",
              "enum": [
                "viewed",
                "edited",
                "uploaded",
                "shared",
                "all"
              ],
              "default": "all"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Recent captures",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "captures": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/TinyTakeCapture"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "last_activity": {
                                "type": "string",
                                "enum": [
                                  "viewed",
                                  "edited",
                                  "uploaded",
                                  "shared"
                                ]
                              },
                              "last_activity_at": {
                                "type": "string",
                                "format": "date-time"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/tinytake/captures/bulk": {
      "post": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Bulk operations on captures",
        "description": "Perform bulk operations on multiple captures at once.\n\nUse this endpoint to:\n- Move multiple captures to folder\n- Add/remove tags from multiple captures\n- Change visibility of multiple captures\n- Delete multiple captures\n\nSupports up to 100 captures per request.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "capture_ids",
                  "operation"
                ],
                "properties": {
                  "capture_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "minItems": 1,
                    "maxItems": 100,
                    "description": "List of capture IDs to operate on"
                  },
                  "operation": {
                    "type": "string",
                    "enum": [
                      "move",
                      "add_tags",
                      "remove_tags",
                      "set_tags",
                      "set_visibility",
                      "delete"
                    ],
                    "description": "Operation to perform"
                  },
                  "folder_id": {
                    "type": "string",
                    "nullable": true,
                    "description": "Target folder (for move operation, null for root)"
                  },
                  "tags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Tags to add/remove/set"
                  },
                  "visibility": {
                    "type": "string",
                    "enum": [
                      "private",
                      "shared",
                      "public"
                    ],
                    "description": "Target visibility (for set_visibility operation)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Bulk operation completed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string",
                      "example": "Successfully updated 15 captures"
                    },
                    "processed_count": {
                      "type": "integer",
                      "example": 15
                    },
                    "failed_count": {
                      "type": "integer",
                      "example": 0
                    },
                    "failures": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "capture_id": {
                            "type": "string"
                          },
                          "error": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/tinytake/captures/search/text": {
      "get": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Search by extracted text",
        "description": "Search captures by their extracted OCR text content.\n\nUse this endpoint to:\n- Find screenshots containing specific text\n- Search by visible text content\n- Full-text search in images\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "query",
            "in": "query",
            "required": true,
            "description": "Text to search for",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "match_type",
            "in": "query",
            "description": "How to match the query",
            "schema": {
              "type": "string",
              "enum": [
                "contains",
                "exact",
                "fuzzy"
              ],
              "default": "contains"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Search results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "query": {
                      "type": "string"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "captures": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/TinyTakeCapture"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "matched_text": {
                                "type": "string",
                                "description": "Text snippet with match highlighted"
                              },
                              "match_count": {
                                "type": "integer",
                                "description": "Number of matches in this capture"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/tinytake/captures/{id}": {
      "get": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Get capture details",
        "description": "Retrieve detailed metadata for a specific capture.\n\nUse this endpoint to:\n- Get capture information\n- View file details\n- Check capture status\n- Get download link\n- View capture metadata\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Capture ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Capture details retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "capture": {
                      "$ref": "#/components/schemas/TinyTakeCaptureDetail"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Capture not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Update capture",
        "description": "Update capture metadata (name, tags, visibility, folder).\n\nUse this endpoint to:\n- Rename capture\n- Update tags\n- Change visibility\n- Move to folder\n- Edit capture details\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Capture ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "New name for the capture",
                    "example": "Updated Meeting Notes"
                  },
                  "tags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "New tags (replaces existing)",
                    "example": [
                      "meeting",
                      "important",
                      "q1"
                    ]
                  },
                  "visibility": {
                    "type": "string",
                    "enum": [
                      "private",
                      "shared",
                      "public"
                    ],
                    "description": "New visibility level"
                  },
                  "folder_id": {
                    "type": "string",
                    "nullable": true,
                    "description": "Move to folder (null for root)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Capture updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string",
                      "example": "Capture updated successfully"
                    },
                    "capture": {
                      "$ref": "#/components/schemas/TinyTakeCaptureDetail"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid parameters",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Capture not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Delete capture",
        "description": "Permanently delete a capture.\n\nUse this endpoint to:\n- Delete screenshot\n- Remove recording\n- Delete capture\n- Remove file from TinyTake\n\n⚠️ This action cannot be undone.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Capture ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Capture deleted successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Capture deleted successfully"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Capture not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/tinytake/captures/{id}/download": {
      "get": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Download capture",
        "description": "Download the original capture file.\n\nUse this endpoint to:\n- Download screenshot\n- Download video recording\n- Get original file\n- Save capture locally\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Capture ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "File content",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/jpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "video/mp4": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "Capture not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/tinytake/captures/{id}/share": {
      "post": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Generate share link",
        "description": "Generate a shareable link for a capture.\n\nUse this endpoint to:\n- Share screenshot\n- Create share link\n- Generate public URL\n- Share recording with others\n- Get shareable link\n\nSupports expiration and optional password protection.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Capture ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "expires_in": {
                    "type": "integer",
                    "description": "Link expiration in seconds (null for permanent)",
                    "example": 86400,
                    "nullable": true
                  },
                  "password": {
                    "type": "string",
                    "description": "Optional password protection",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Share link generated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "share_url": {
                      "type": "string",
                      "format": "uri",
                      "example": "https://hub.mangoapps.com/tt/abc123"
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "has_password": {
                      "type": "boolean",
                      "example": false
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Capture not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Revoke share link",
        "description": "Revoke an existing share link for a capture.\n\nUse this endpoint to:\n- Stop sharing\n- Revoke access\n- Disable share link\n- Remove public access\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Capture ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Share link revoked successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Share link revoked"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Capture not found or no share link exists"
          }
        }
      }
    },
    "/tinytake/captures/{id}/share/users": {
      "get": {
        "tags": [
          "TinyTake"
        ],
        "summary": "List users capture is shared with",
        "description": "Get list of users and teams this capture is shared with.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Share list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "shares": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TinyTakeShare"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Capture not found"
          }
        }
      },
      "post": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Share with users/teams",
        "description": "Share a capture with specific users or teams.\n\nUse this endpoint to:\n- Share with specific users\n- Share with team\n- Grant view/comment/edit access\n- Send notification to recipients\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "recipients"
                ],
                "properties": {
                  "recipients": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "type",
                        "id"
                      ],
                      "properties": {
                        "type": {
                          "type": "string",
                          "enum": [
                            "user",
                            "team"
                          ]
                        },
                        "id": {
                          "type": "string",
                          "description": "User ID or Team ID"
                        }
                      }
                    }
                  },
                  "permission": {
                    "type": "string",
                    "enum": [
                      "view",
                      "comment",
                      "edit"
                    ],
                    "default": "view"
                  },
                  "notify": {
                    "type": "boolean",
                    "default": true,
                    "description": "Send notification to recipients"
                  },
                  "message": {
                    "type": "string",
                    "description": "Optional message to include in notification"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Capture shared successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string",
                      "example": "Shared with 3 users"
                    },
                    "shares": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TinyTakeShare"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Capture not found"
          }
        }
      },
      "delete": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Revoke user/team access",
        "description": "Remove sharing access from specific users or teams.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "recipients"
                ],
                "properties": {
                  "recipients": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "type",
                        "id"
                      ],
                      "properties": {
                        "type": {
                          "type": "string",
                          "enum": [
                            "user",
                            "team"
                          ]
                        },
                        "id": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Access revoked",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string",
                      "example": "Removed access for 2 users"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/tinytake/captures/{id}/annotations": {
      "get": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Get capture annotations",
        "description": "Retrieve all annotations on a capture (arrows, text, shapes, blur regions).\n\nUse this endpoint to:\n- Get annotations to display\n- Load annotations for editing\n- Check if capture has annotations\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Capture annotations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "capture_id": {
                      "type": "string"
                    },
                    "annotations": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TinyTakeAnnotation"
                      }
                    },
                    "canvas_width": {
                      "type": "integer",
                      "description": "Original canvas width for scaling"
                    },
                    "canvas_height": {
                      "type": "integer",
                      "description": "Original canvas height for scaling"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Capture not found"
          }
        }
      },
      "put": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Save capture annotations",
        "description": "Save or update all annotations on a capture.\n\nUse this endpoint to:\n- Save annotation edits\n- Add new annotations\n- Update existing annotations\n- Remove annotations (by omitting them)\n\nReplaces all existing annotations with the provided set.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "annotations"
                ],
                "properties": {
                  "annotations": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/TinyTakeAnnotationInput"
                    }
                  },
                  "canvas_width": {
                    "type": "integer",
                    "description": "Canvas width used when creating annotations"
                  },
                  "canvas_height": {
                    "type": "integer",
                    "description": "Canvas height used when creating annotations"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Annotations saved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string",
                      "example": "Annotations saved successfully"
                    },
                    "annotation_count": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid annotations"
          },
          "404": {
            "description": "Capture not found"
          }
        }
      }
    },
    "/tinytake/captures/{id}/flatten": {
      "post": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Flatten annotations",
        "description": "Flatten annotations into the image, creating a new version.\n\nUse this endpoint to:\n- Burn annotations into image\n- Create shareable version with annotations\n- Export annotated image\n\nOriginal capture is preserved; creates a new capture with flattened annotations.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Name for the flattened capture (defaults to original + \"_annotated\")"
                  },
                  "replace_original": {
                    "type": "boolean",
                    "default": false,
                    "description": "Replace original instead of creating new capture"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Flattened capture created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    },
                    "capture": {
                      "$ref": "#/components/schemas/TinyTakeCaptureDetail"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Capture has no annotations"
          },
          "404": {
            "description": "Capture not found"
          }
        }
      }
    },
    "/tinytake/captures/{id}/comments": {
      "get": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Get capture comments",
        "description": "Retrieve all comments on a capture.\n\nUse this endpoint to:\n- View comments\n- Load comment thread\n- Get feedback on capture\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort_order",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "asc"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Capture comments",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "capture_id": {
                      "type": "string"
                    },
                    "comment_count": {
                      "type": "integer"
                    },
                    "comments": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TinyTakeComment"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Capture not found"
          }
        }
      },
      "post": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Add comment to capture",
        "description": "Add a comment to a capture. Supports optional position anchoring for contextual comments.\n\nUse this endpoint to:\n- Add feedback\n- Comment on specific area\n- Reply to existing comment\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "content"
                ],
                "properties": {
                  "content": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 2000,
                    "description": "Comment text (supports markdown)"
                  },
                  "parent_id": {
                    "type": "string",
                    "nullable": true,
                    "description": "Parent comment ID for replies"
                  },
                  "position": {
                    "type": "object",
                    "description": "Optional position anchor for contextual comments",
                    "properties": {
                      "x": {
                        "type": "number",
                        "description": "X coordinate (percentage 0-100)"
                      },
                      "y": {
                        "type": "number",
                        "description": "Y coordinate (percentage 0-100)"
                      },
                      "timestamp": {
                        "type": "number",
                        "description": "Video timestamp in seconds (for video comments)"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Comment added",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "comment": {
                      "$ref": "#/components/schemas/TinyTakeComment"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid comment"
          },
          "404": {
            "description": "Capture not found"
          }
        }
      }
    },
    "/tinytake/captures/{id}/comments/{comment_id}": {
      "patch": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Update comment",
        "description": "Edit an existing comment (author only).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "comment_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "content"
                ],
                "properties": {
                  "content": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 2000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Comment updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "comment": {
                      "$ref": "#/components/schemas/TinyTakeComment"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not authorized to edit this comment"
          },
          "404": {
            "description": "Comment not found"
          }
        }
      },
      "delete": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Delete comment",
        "description": "Delete a comment (author or capture owner only).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "comment_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Comment deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not authorized to delete this comment"
          },
          "404": {
            "description": "Comment not found"
          }
        }
      }
    },
    "/tinytake/captures/{id}/stream": {
      "get": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Get video stream URL",
        "description": "Get a streaming URL for video playback.\n\nUse this endpoint to:\n- Get video playback URL\n- Stream video in browser\n- Get adaptive streaming manifest\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "quality",
            "in": "query",
            "description": "Preferred quality (if available)",
            "schema": {
              "type": "string",
              "enum": [
                "auto",
                "360p",
                "480p",
                "720p",
                "1080p",
                "original"
              ],
              "default": "auto"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Streaming information",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "stream_url": {
                      "type": "string",
                      "format": "uri",
                      "description": "Direct streaming URL"
                    },
                    "hls_url": {
                      "type": "string",
                      "format": "uri",
                      "nullable": true,
                      "description": "HLS manifest URL (if available)"
                    },
                    "available_qualities": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "duration_seconds": {
                      "type": "number"
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "When the stream URL expires"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Not a video capture"
          },
          "404": {
            "description": "Capture not found"
          }
        }
      }
    },
    "/tinytake/captures/{id}/status": {
      "get": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Get processing status",
        "description": "Get the processing/transcoding status of a capture.\n\nUse this endpoint to:\n- Check upload processing status\n- Monitor video transcoding\n- Check thumbnail generation\n- Poll for completion\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Processing status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "capture_id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "uploading",
                        "processing",
                        "transcoding",
                        "ready",
                        "failed"
                      ]
                    },
                    "progress": {
                      "type": "integer",
                      "description": "Progress percentage (0-100)"
                    },
                    "stages": {
                      "type": "object",
                      "properties": {
                        "upload": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "in_progress",
                            "completed",
                            "failed"
                          ]
                        },
                        "thumbnail": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "in_progress",
                            "completed",
                            "failed",
                            "skipped"
                          ]
                        },
                        "transcode": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "in_progress",
                            "completed",
                            "failed",
                            "skipped"
                          ]
                        },
                        "ocr": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "in_progress",
                            "completed",
                            "failed",
                            "skipped"
                          ]
                        }
                      }
                    },
                    "error": {
                      "type": "string",
                      "nullable": true,
                      "description": "Error message if status is failed"
                    },
                    "estimated_completion": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Capture not found"
          }
        }
      }
    },
    "/tinytake/captures/{id}/chapters": {
      "get": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Get video chapters",
        "description": "Get chapter markers for a video.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Video chapters",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "chapters": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TinyTakeVideoChapter"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Not a video capture"
          },
          "404": {
            "description": "Capture not found"
          }
        }
      },
      "put": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Set video chapters",
        "description": "Add or update chapter markers for a video.\n\nUse this endpoint to:\n- Add chapter navigation\n- Mark important sections\n- Create table of contents\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "chapters"
                ],
                "properties": {
                  "chapters": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "timestamp",
                        "title"
                      ],
                      "properties": {
                        "timestamp": {
                          "type": "number",
                          "description": "Start time in seconds"
                        },
                        "title": {
                          "type": "string",
                          "maxLength": 100
                        },
                        "description": {
                          "type": "string",
                          "maxLength": 500
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Chapters saved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "chapters": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TinyTakeVideoChapter"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Not a video or invalid chapters"
          },
          "404": {
            "description": "Capture not found"
          }
        }
      }
    },
    "/tinytake/captures/{id}/trim": {
      "post": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Trim video",
        "description": "Create a trimmed version of a video.\n\nUse this endpoint to:\n- Trim video start/end\n- Extract video clip\n- Remove unwanted sections\n\nCreates a new capture with the trimmed video.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "start_time",
                  "end_time"
                ],
                "properties": {
                  "start_time": {
                    "type": "number",
                    "description": "Start timestamp in seconds",
                    "minimum": 0
                  },
                  "end_time": {
                    "type": "number",
                    "description": "End timestamp in seconds"
                  },
                  "name": {
                    "type": "string",
                    "description": "Name for trimmed video (defaults to original + \"_trimmed\")"
                  },
                  "replace_original": {
                    "type": "boolean",
                    "default": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Trim job started",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string",
                      "example": "Trim job started"
                    },
                    "job_id": {
                      "type": "string"
                    },
                    "estimated_duration": {
                      "type": "integer",
                      "description": "Estimated processing time in seconds"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid time range or not a video"
          },
          "404": {
            "description": "Capture not found"
          }
        }
      }
    },
    "/tinytake/captures/{id}/gif": {
      "post": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Convert to GIF",
        "description": "Convert a video clip to an animated GIF.\n\nUse this endpoint to:\n- Create shareable GIF\n- Convert video to animated image\n- Make preview/thumbnail GIF\n\nCreates a new capture with the generated GIF.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "start_time": {
                    "type": "number",
                    "description": "Start timestamp (defaults to 0)",
                    "default": 0
                  },
                  "end_time": {
                    "type": "number",
                    "description": "End timestamp (defaults to video end, max 30 seconds)"
                  },
                  "fps": {
                    "type": "integer",
                    "description": "Frames per second",
                    "default": 10,
                    "minimum": 5,
                    "maximum": 30
                  },
                  "width": {
                    "type": "integer",
                    "description": "Output width (height auto-calculated)",
                    "maximum": 800
                  },
                  "quality": {
                    "type": "string",
                    "enum": [
                      "low",
                      "medium",
                      "high"
                    ],
                    "default": "medium"
                  },
                  "name": {
                    "type": "string",
                    "description": "Name for generated GIF"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "GIF generation started",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string",
                      "example": "GIF generation started"
                    },
                    "job_id": {
                      "type": "string"
                    },
                    "estimated_duration": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Not a video or invalid parameters"
          },
          "404": {
            "description": "Capture not found"
          }
        }
      }
    },
    "/tinytake/captures/{id}/extract-text": {
      "post": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Extract text (OCR)",
        "description": "Extract text from a screenshot using OCR.\n\nUse this endpoint to:\n- Extract text from image\n- Make screenshots searchable\n- Copy text from screenshot\n\nResults are stored and used for search indexing.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "languages": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Language hints for OCR (ISO 639-1 codes)",
                    "example": [
                      "en",
                      "es"
                    ]
                  },
                  "force": {
                    "type": "boolean",
                    "default": false,
                    "description": "Re-run OCR even if already processed"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Text extracted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "capture_id": {
                      "type": "string"
                    },
                    "text": {
                      "type": "string",
                      "description": "Full extracted text"
                    },
                    "blocks": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "text": {
                            "type": "string"
                          },
                          "confidence": {
                            "type": "number"
                          },
                          "bounding_box": {
                            "type": "object",
                            "properties": {
                              "x": {
                                "type": "number"
                              },
                              "y": {
                                "type": "number"
                              },
                              "width": {
                                "type": "number"
                              },
                              "height": {
                                "type": "number"
                              }
                            }
                          }
                        }
                      }
                    },
                    "language_detected": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Not an image capture"
          },
          "404": {
            "description": "Capture not found"
          }
        }
      }
    },
    "/tinytake/captures/{id}/describe": {
      "post": {
        "tags": [
          "TinyTake"
        ],
        "summary": "AI describe capture",
        "description": "Generate an AI description of the capture content.\n\nUse this endpoint to:\n- Generate alt-text\n- Create automatic description\n- Summarize screenshot content\n- Describe video content\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "style": {
                    "type": "string",
                    "enum": [
                      "brief",
                      "detailed",
                      "technical",
                      "accessibility"
                    ],
                    "default": "brief",
                    "description": "Description style"
                  },
                  "save_as_description": {
                    "type": "boolean",
                    "default": false,
                    "description": "Save as capture description"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Description generated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "description": {
                      "type": "string"
                    },
                    "suggested_tags": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "detected_elements": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "UI elements, text, objects detected"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Capture not found"
          }
        }
      }
    },
    "/tinytake/captures/{id}/analytics": {
      "get": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Get capture analytics",
        "description": "Get view and download statistics for a capture.\n\nUse this endpoint to:\n- See how many views\n- Track downloads\n- Monitor share engagement\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "period",
            "in": "query",
            "description": "Time period for analytics",
            "schema": {
              "type": "string",
              "enum": [
                "day",
                "week",
                "month",
                "year",
                "all"
              ],
              "default": "month"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Capture analytics",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "capture_id": {
                      "type": "string"
                    },
                    "period": {
                      "type": "string"
                    },
                    "totals": {
                      "type": "object",
                      "properties": {
                        "views": {
                          "type": "integer"
                        },
                        "unique_viewers": {
                          "type": "integer"
                        },
                        "downloads": {
                          "type": "integer"
                        },
                        "shares": {
                          "type": "integer"
                        },
                        "comments": {
                          "type": "integer"
                        }
                      }
                    },
                    "timeline": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "date": {
                            "type": "string",
                            "format": "date"
                          },
                          "views": {
                            "type": "integer"
                          },
                          "downloads": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Capture not found"
          }
        }
      }
    },
    "/tinytake/tags": {
      "get": {
        "tags": [
          "TinyTake"
        ],
        "summary": "List all tags",
        "description": "Retrieve all tags used across captures with usage counts.\n\nUse this endpoint to:\n- Get all available tags\n- See tag usage statistics\n- Build tag cloud/selector\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "sort_by",
            "in": "query",
            "description": "Sort tags by",
            "schema": {
              "type": "string",
              "enum": [
                "name",
                "count",
                "recent"
              ],
              "default": "count"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum tags to return",
            "schema": {
              "type": "integer",
              "default": 100,
              "maximum": 500
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of tags",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "tags": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TinyTakeTag"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/tinytake/shared": {
      "get": {
        "tags": [
          "TinyTake"
        ],
        "summary": "List captures shared with me",
        "description": "Retrieve captures that others have shared with you.\n\nUse this endpoint to:\n- View shared captures\n- See what's been shared with me\n- Access team shared content\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "shared_by",
            "in": "query",
            "description": "Filter by user who shared",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort_by",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "shared_at",
                "name",
                "created_at"
              ],
              "default": "shared_at"
            }
          },
          {
            "name": "sort_order",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Shared captures",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "captures": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/TinyTakeCapture"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "shared_by": {
                                "$ref": "#/components/schemas/TinyTakeUser"
                              },
                              "shared_at": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "permission": {
                                "type": "string",
                                "enum": [
                                  "view",
                                  "comment",
                                  "edit"
                                ]
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/tinytake/activity": {
      "get": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Get activity log",
        "description": "Get user's TinyTake activity history.\n\nUse this endpoint to:\n- View activity history\n- Audit trail\n- See recent actions\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "action",
            "in": "query",
            "description": "Filter by action type",
            "schema": {
              "type": "string",
              "enum": [
                "upload",
                "view",
                "download",
                "share",
                "delete",
                "edit",
                "comment"
              ]
            }
          },
          {
            "name": "capture_id",
            "in": "query",
            "description": "Filter by specific capture",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "before",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Activity log",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "activities": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TinyTakeActivity"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/tinytake/stats": {
      "get": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Get usage statistics",
        "description": "Get overall TinyTake usage statistics.\n\nUse this endpoint to:\n- View usage summary\n- Check capture statistics\n- Monitor trends\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "period",
            "in": "query",
            "description": "Time period for stats",
            "schema": {
              "type": "string",
              "enum": [
                "week",
                "month",
                "quarter",
                "year",
                "all"
              ],
              "default": "month"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Usage statistics",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "period": {
                      "type": "string"
                    },
                    "summary": {
                      "type": "object",
                      "properties": {
                        "total_captures": {
                          "type": "integer"
                        },
                        "total_images": {
                          "type": "integer"
                        },
                        "total_videos": {
                          "type": "integer"
                        },
                        "total_size_bytes": {
                          "type": "integer",
                          "format": "int64"
                        },
                        "captures_this_period": {
                          "type": "integer"
                        },
                        "storage_used_percentage": {
                          "type": "number"
                        }
                      }
                    },
                    "trends": {
                      "type": "object",
                      "properties": {
                        "captures_trend": {
                          "type": "number",
                          "description": "Percentage change from previous period"
                        },
                        "storage_trend": {
                          "type": "number"
                        }
                      }
                    },
                    "top_tags": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TinyTakeTag"
                      }
                    },
                    "by_type": {
                      "type": "object",
                      "properties": {
                        "image": {
                          "type": "integer"
                        },
                        "video": {
                          "type": "integer"
                        }
                      }
                    },
                    "timeline": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "date": {
                            "type": "string",
                            "format": "date"
                          },
                          "captures": {
                            "type": "integer"
                          },
                          "size_bytes": {
                            "type": "integer",
                            "format": "int64"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/tinytake/folders": {
      "get": {
        "tags": [
          "TinyTake"
        ],
        "summary": "List folders",
        "description": "Retrieve all folders for organizing captures.\n\nUse this endpoint to:\n- List my folders\n- Get folder structure\n- View folder hierarchy\n- Browse folders\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "parent_id",
            "in": "query",
            "description": "Parent folder ID (null for root folders)",
            "schema": {
              "type": "string",
              "nullable": true
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of folders",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "folders": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TinyTakeFolder"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Create folder",
        "description": "Create a new folder for organizing captures.\n\nUse this endpoint to:\n- Create new folder\n- Add folder\n- Organize captures\n- Create subfolder\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Folder name",
                    "example": "Project Screenshots"
                  },
                  "parent_id": {
                    "type": "string",
                    "nullable": true,
                    "description": "Parent folder ID (null for root)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Folder created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string",
                      "example": "Folder created successfully"
                    },
                    "folder": {
                      "$ref": "#/components/schemas/TinyTakeFolder"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid folder name"
          },
          "409": {
            "description": "Folder with this name already exists"
          }
        }
      }
    },
    "/tinytake/folders/{id}": {
      "patch": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Update folder",
        "description": "Rename or move a folder.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "New folder name"
                  },
                  "parent_id": {
                    "type": "string",
                    "nullable": true,
                    "description": "New parent folder (null for root)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Folder updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "folder": {
                      "$ref": "#/components/schemas/TinyTakeFolder"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Folder not found"
          }
        }
      },
      "delete": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Delete folder",
        "description": "Delete a folder. Captures in the folder are moved to root, not deleted.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Folder deleted successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string",
                      "example": "Folder deleted. 5 captures moved to root."
                    },
                    "moved_captures_count": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Folder not found"
          }
        }
      }
    },
    "/tinytake/storage": {
      "get": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Get storage info",
        "description": "Retrieve storage usage, limits, and quota information.\n\nUse this endpoint to:\n- Check storage usage\n- View storage limits\n- Check quota\n- See remaining storage\n- Get storage statistics\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Storage information",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "storage": {
                      "$ref": "#/components/schemas/TinyTakeStorageInfo"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/tinytake/settings": {
      "get": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Get recorder settings",
        "description": "Retrieve TinyTake client settings including hotkeys and preferences.\n\nUse this endpoint to:\n- Get hotkey configuration\n- View recorder settings\n- Check default preferences\n- Get client configuration\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Recorder settings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "settings": {
                      "$ref": "#/components/schemas/TinyTakeSettings"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Update recorder settings",
        "description": "Update TinyTake client preferences.\n\nUse this endpoint to:\n- Update hotkeys\n- Change default settings\n- Configure preferences\n- Set default format\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TinyTakeSettingsUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Settings updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string",
                      "example": "Settings updated successfully"
                    },
                    "settings": {
                      "$ref": "#/components/schemas/TinyTakeSettings"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid settings"
          }
        }
      }
    },
    "/tinytake/version": {
      "get": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Check for updates",
        "description": "Check if a newer version of TinyTake client is available.\n\nUse this endpoint to:\n- Check for updates\n- Get latest version info\n- Check if upgrade available\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "current_version",
            "in": "query",
            "required": true,
            "description": "Current client version",
            "schema": {
              "type": "string"
            },
            "example": "18.2.40"
          },
          {
            "name": "platform",
            "in": "query",
            "required": true,
            "description": "Client platform",
            "schema": {
              "type": "string",
              "enum": [
                "windows",
                "macos",
                "linux"
              ]
            }
          },
          {
            "name": "os_version",
            "in": "query",
            "description": "Operating system version",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Version check result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "update_available": {
                      "type": "boolean",
                      "example": true
                    },
                    "current_version": {
                      "type": "string",
                      "example": "18.2.40"
                    },
                    "latest_version": {
                      "type": "string",
                      "example": "18.3.0"
                    },
                    "release_notes": {
                      "type": "string",
                      "nullable": true
                    },
                    "download_url": {
                      "type": "string",
                      "format": "uri",
                      "nullable": true
                    },
                    "is_mandatory": {
                      "type": "boolean",
                      "example": false
                    },
                    "published_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/tinytake/downloads": {
      "get": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Get download URLs",
        "description": "Retrieve download URLs for TinyTake client installers.\n\nUse this endpoint to:\n- Get installer download link\n- Download TinyTake client\n- Get platform-specific installer\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Download URLs for all platforms",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "downloads": {
                      "type": "object",
                      "properties": {
                        "windows": {
                          "type": "object",
                          "properties": {
                            "installer": {
                              "type": "string",
                              "format": "uri"
                            },
                            "portable": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true
                            },
                            "version": {
                              "type": "string"
                            }
                          }
                        },
                        "macos": {
                          "type": "object",
                          "properties": {
                            "installer": {
                              "type": "string",
                              "format": "uri"
                            },
                            "version": {
                              "type": "string"
                            }
                          }
                        },
                        "linux": {
                          "type": "object",
                          "properties": {
                            "deb": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true
                            },
                            "rpm": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true
                            },
                            "appimage": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true
                            },
                            "version": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/tinytake/feedback": {
      "post": {
        "tags": [
          "TinyTake"
        ],
        "summary": "Submit feedback",
        "description": "Submit feedback or report a problem from TinyTake client.\n\nUse this endpoint to:\n- Report a bug\n- Submit feedback\n- Request feature\n- Report problem\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "type",
                  "message"
                ],
                "properties": {
                  "type": {
                    "type": "string",
                    "enum": [
                      "bug",
                      "feature_request",
                      "question",
                      "other"
                    ],
                    "description": "Type of feedback",
                    "example": "bug"
                  },
                  "message": {
                    "type": "string",
                    "minLength": 10,
                    "maxLength": 5000,
                    "description": "Feedback message",
                    "example": "Screen recording stops unexpectedly after 5 minutes"
                  },
                  "attachments": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Capture IDs to attach as evidence",
                    "maxItems": 5
                  },
                  "system_info": {
                    "type": "object",
                    "properties": {
                      "os_name": {
                        "type": "string"
                      },
                      "os_version": {
                        "type": "string"
                      },
                      "app_version": {
                        "type": "string"
                      },
                      "device_id": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Feedback submitted successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Thank you for your feedback"
                    },
                    "feedback_id": {
                      "type": "string",
                      "example": "fb_abc123"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid feedback",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/compensation/profile": {
      "get": {
        "tags": [
          "Employee Compensation"
        ],
        "summary": "Get employee compensation profile",
        "description": "Retrieves the current employee's compensation profile including current compensation details,\npolicies, and eligibility information. Respects organization compensation policies for employee self-view.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Employee compensation profile retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "compensation_profile": {
                      "type": "object",
                      "description": "Employee compensation profile with current details and policies",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "description": "User business ID",
                          "example": 123
                        },
                        "employee": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            },
                            "title": {
                              "type": "string",
                              "nullable": true,
                              "example": "Software Engineer"
                            },
                            "hire_date": {
                              "type": "string",
                              "format": "date",
                              "nullable": true,
                              "example": "2023-01-15"
                            }
                          }
                        },
                        "current_compensation": {
                          "type": "object",
                          "properties": {
                            "type": {
                              "type": "string",
                              "enum": [
                                "salary",
                                "hourly",
                                "commission",
                                "contract"
                              ],
                              "example": "salary"
                            },
                            "annual_salary": {
                              "type": "number",
                              "nullable": true,
                              "example": 75000.0
                            },
                            "hourly_rate": {
                              "type": "number",
                              "nullable": true,
                              "example": 36.06
                            },
                            "currency": {
                              "type": "string",
                              "example": "USD"
                            },
                            "pay_frequency": {
                              "type": "string",
                              "enum": [
                                "weekly",
                                "biweekly",
                                "monthly",
                                "quarterly",
                                "annually"
                              ],
                              "example": "monthly"
                            },
                            "effective_date": {
                              "type": "string",
                              "format": "date",
                              "nullable": true,
                              "example": "2024-01-01"
                            }
                          }
                        },
                        "policies": {
                          "type": "object",
                          "properties": {
                            "can_view_details": {
                              "type": "boolean",
                              "example": true
                            },
                            "can_request_changes": {
                              "type": "boolean",
                              "example": true
                            },
                            "request_frequency": {
                              "type": "string",
                              "enum": [
                                "annual",
                                "semi_annual",
                                "unlimited"
                              ],
                              "example": "annual"
                            },
                            "mobile_access_enabled": {
                              "type": "boolean",
                              "example": true
                            }
                          }
                        },
                        "next_review_eligible": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "example": "2024-12-01"
                        },
                        "last_updated": {
                          "type": "object",
                          "properties": {
                            "date": {
                              "type": "string",
                              "format": "date-time",
                              "example": "2024-01-15T10:30:00Z"
                            },
                            "by": {
                              "type": "string",
                              "example": "System"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/compensation/history": {
      "get": {
        "tags": [
          "Employee Compensation"
        ],
        "summary": "Get compensation change history",
        "description": "Retrieves the employee's compensation change history with pagination and filtering options.\nShows historical compensation changes, reasons, and approval information.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Number of items per page. Values above 100 are clamped to 100;\nblank, non-numeric and non-positive values fall back to the default.\n",
            "schema": {
              "type": "integer",
              "default": 10,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "from_date",
            "in": "query",
            "description": "Filter changes on or after this date (YYYY-MM-DD). Inclusive — the\nbound is midnight (UTC) at the start of the day.\n",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to_date",
            "in": "query",
            "description": "Filter changes on or before this date (YYYY-MM-DD). Inclusive of the\nWHOLE day — the bound is 23:59:59.999999 (UTC).\nfilter-search-audit 2026-09-02: the description already read as\ninclusive while the query bound was `change_date <= <midnight>`, which\nexcluded every change recorded later that day (2,162 of 2,168 rows\ncarry a non-midnight time). The scope was fixed to match this wording,\nnot the other way round.\n",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "400": {
            "description": "Bad request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "200": {
            "description": "Compensation history retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "Individual compensation change record",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 789
                          },
                          "change_date": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2024-01-15T10:30:00Z"
                          },
                          "effective_date": {
                            "type": "string",
                            "format": "date",
                            "example": "2024-02-01"
                          },
                          "change_reason": {
                            "type": "string",
                            "example": "Annual merit increase"
                          },
                          "changed_by": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 101
                              },
                              "name": {
                                "type": "string",
                                "example": "Jane Manager"
                              }
                            }
                          },
                          "changes": {
                            "type": "object",
                            "properties": {
                              "compensation_type": {
                                "type": "object",
                                "properties": {
                                  "from": {
                                    "type": "string",
                                    "nullable": true,
                                    "example": "salary"
                                  },
                                  "to": {
                                    "type": "string",
                                    "nullable": true,
                                    "example": "salary"
                                  }
                                }
                              },
                              "annual_salary": {
                                "type": "object",
                                "properties": {
                                  "from": {
                                    "type": "number",
                                    "nullable": true,
                                    "example": 70000.0
                                  },
                                  "to": {
                                    "type": "number",
                                    "nullable": true,
                                    "example": 75000.0
                                  }
                                }
                              },
                              "hourly_rate": {
                                "type": "object",
                                "properties": {
                                  "from": {
                                    "type": "number",
                                    "nullable": true,
                                    "example": null
                                  },
                                  "to": {
                                    "type": "number",
                                    "nullable": true,
                                    "example": null
                                  }
                                }
                              },
                              "pay_frequency": {
                                "type": "object",
                                "properties": {
                                  "from": {
                                    "type": "string",
                                    "nullable": true,
                                    "example": "monthly"
                                  },
                                  "to": {
                                    "type": "string",
                                    "nullable": true,
                                    "example": "monthly"
                                  }
                                }
                              },
                              "currency": {
                                "type": "object",
                                "properties": {
                                  "from": {
                                    "type": "string",
                                    "nullable": true,
                                    "example": "USD"
                                  },
                                  "to": {
                                    "type": "string",
                                    "nullable": true,
                                    "example": "USD"
                                  }
                                }
                              }
                            }
                          },
                          "change_summary": {
                            "type": "string",
                            "example": "Salary: $70,000 → $75,000"
                          },
                          "change_direction": {
                            "type": "string",
                            "enum": [
                              "increase",
                              "decrease",
                              "neutral"
                            ],
                            "example": "increase"
                          },
                          "is_retroactive": {
                            "type": "boolean",
                            "example": false
                          },
                          "effective_soon": {
                            "type": "boolean",
                            "example": true
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 6
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Items per page",
                          "example": 25
                        }
                      },
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/compensation/insights": {
      "get": {
        "tags": [
          "Employee Compensation"
        ],
        "summary": "Get personalized compensation insights",
        "description": "Provides personalized compensation insights including tenure analysis, recent activity,\nrequest eligibility, and performance integration information.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Compensation insights retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "insights": {
                      "type": "object",
                      "description": "Personalized compensation insights and analytics",
                      "properties": {
                        "summary": {
                          "type": "object",
                          "properties": {
                            "current_annualized_value": {
                              "type": "number",
                              "nullable": true,
                              "example": 75000.0
                            },
                            "compensation_type": {
                              "type": "string",
                              "example": "salary"
                            },
                            "currency": {
                              "type": "string",
                              "example": "USD"
                            },
                            "tenure_months": {
                              "type": "integer",
                              "example": 18
                            }
                          }
                        },
                        "recent_activity": {
                          "type": "object",
                          "properties": {
                            "changes_last_12_months": {
                              "type": "integer",
                              "example": 1
                            },
                            "last_change_date": {
                              "type": "string",
                              "format": "date",
                              "nullable": true,
                              "example": "2024-01-15"
                            }
                          }
                        },
                        "request_eligibility": {
                          "type": "object",
                          "properties": {
                            "can_request_now": {
                              "type": "boolean",
                              "example": true
                            },
                            "next_eligible_date": {
                              "type": "string",
                              "format": "date",
                              "nullable": true,
                              "example": "2025-01-15"
                            }
                          }
                        },
                        "performance_link": {
                          "type": "object",
                          "properties": {
                            "has_recent_review": {
                              "type": "boolean",
                              "example": false
                            },
                            "merit_increase_eligible": {
                              "type": "boolean",
                              "example": false
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/compensation/calculate": {
      "post": {
        "tags": [
          "Employee Compensation"
        ],
        "summary": "Calculate compensation conversions",
        "description": "Performs real-time compensation calculations including salary to hourly conversions\nand vice versa based on configurable annual hours.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "calculation"
                ],
                "properties": {
                  "calculation": {
                    "type": "object",
                    "required": [
                      "compensation_type"
                    ],
                    "properties": {
                      "compensation_type": {
                        "type": "string",
                        "enum": [
                          "salary",
                          "hourly"
                        ],
                        "description": "Type of compensation to calculate"
                      },
                      "annual_salary": {
                        "type": "number",
                        "minimum": 0,
                        "description": "Annual salary amount (required for salary type)"
                      },
                      "hourly_rate": {
                        "type": "number",
                        "minimum": 0,
                        "description": "Hourly rate amount (required for hourly type)"
                      },
                      "annual_hours": {
                        "type": "number",
                        "minimum": 1,
                        "default": 2080,
                        "description": "Annual working hours for calculation basis"
                      },
                      "currency": {
                        "type": "string",
                        "default": "USD",
                        "description": "Currency code"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation completed successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "calculation": {
                      "type": "object",
                      "description": "Compensation calculation result",
                      "properties": {
                        "compensation_type": {
                          "type": "string",
                          "enum": [
                            "salary",
                            "hourly"
                          ],
                          "example": "salary"
                        },
                        "annual_salary": {
                          "type": "number",
                          "nullable": true,
                          "example": 75000.0
                        },
                        "calculated_hourly_rate": {
                          "type": "number",
                          "nullable": true,
                          "example": 36.06
                        },
                        "hourly_rate": {
                          "type": "number",
                          "nullable": true,
                          "example": null
                        },
                        "calculated_annual_salary": {
                          "type": "number",
                          "nullable": true,
                          "example": null
                        },
                        "annual_hours_basis": {
                          "type": "number",
                          "example": 2080.0
                        },
                        "currency": {
                          "type": "string",
                          "example": "USD"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/compensation/requests": {
      "get": {
        "tags": [
          "Compensation Requests"
        ],
        "summary": "List employee compensation requests",
        "description": "Retrieves all compensation change requests submitted by the current employee\nwith filtering and pagination options.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Number of items per page. Values above 100 are clamped to 100;\nblank, non-numeric and non-positive values fall back to the default.\n",
            "schema": {
              "type": "integer",
              "default": 10,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by request status. Case-sensitive, and a single scalar value.\nfilter-search-audit 2026-09-02: an unrecognized value used to be\nSILENTLY IGNORED, returning every status under an unfiltered\nmeta.total_count (?status=PENDING and ?status=garbage each returned\nall 170 rows for a user whose pending count is 10). It now answers\n400 invalid_status, which this path did not previously declare.\n",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "approved",
                "rejected",
                "cancelled"
              ]
            }
          }
        ],
        "responses": {
          "400": {
            "description": "Bad request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "200": {
            "description": "Compensation requests retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "Summary view of compensation request",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 456
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "pending",
                              "approved",
                              "rejected",
                              "cancelled"
                            ],
                            "example": "pending"
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2024-01-20T14:30:00Z"
                          },
                          "effective_date": {
                            "type": "string",
                            "format": "date",
                            "example": "2024-03-01"
                          },
                          "change_summary": {
                            "type": "string",
                            "example": "Salary: $75,000 → $80,000"
                          },
                          "change_impact": {
                            "type": "object",
                            "properties": {
                              "type": {
                                "type": "string",
                                "enum": [
                                  "increase",
                                  "decrease",
                                  "neutral"
                                ],
                                "example": "increase"
                              },
                              "amount": {
                                "type": "number",
                                "example": 5000.0
                              },
                              "percentage": {
                                "type": "number",
                                "nullable": true,
                                "example": 6.7
                              },
                              "description": {
                                "type": "string",
                                "example": "Increase of $5,000 (6.7%)"
                              }
                            }
                          },
                          "approver": {
                            "type": "object",
                            "nullable": true,
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 789
                              },
                              "name": {
                                "type": "string",
                                "example": "Jane Manager"
                              }
                            }
                          },
                          "days_pending": {
                            "type": "integer",
                            "example": 5
                          },
                          "is_urgent": {
                            "type": "boolean",
                            "example": false
                          },
                          "effective_soon": {
                            "type": "boolean",
                            "example": true
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 6
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Items per page",
                          "example": 25
                        }
                      },
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Compensation Requests"
        ],
        "summary": "Create compensation change request",
        "description": "Creates a new compensation change request. Validates against organization policies\nincluding request frequency limits and approval workflows.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "compensation_request"
                ],
                "properties": {
                  "compensation_request": {
                    "type": "object",
                    "description": "Input schema for creating/updating compensation requests",
                    "required": [
                      "justification",
                      "requested_compensation_type"
                    ],
                    "properties": {
                      "justification": {
                        "type": "string",
                        "minLength": 10,
                        "maxLength": 1000,
                        "example": "Requesting salary increase based on performance review and market analysis"
                      },
                      "effective_date": {
                        "type": "string",
                        "format": "date",
                        "example": "2024-03-01"
                      },
                      "requested_compensation_type": {
                        "type": "string",
                        "enum": [
                          "salary",
                          "hourly",
                          "commission",
                          "contract"
                        ],
                        "example": "salary"
                      },
                      "requested_annual_salary": {
                        "type": "number",
                        "minimum": 0,
                        "nullable": true,
                        "example": 80000.0
                      },
                      "requested_hourly_rate": {
                        "type": "number",
                        "minimum": 0,
                        "nullable": true,
                        "example": null
                      },
                      "requested_pay_frequency": {
                        "type": "string",
                        "enum": [
                          "weekly",
                          "biweekly",
                          "monthly",
                          "quarterly",
                          "annually"
                        ],
                        "example": "monthly"
                      },
                      "requested_currency": {
                        "type": "string",
                        "example": "USD"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Compensation request created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "compensation_request": {
                      "type": "object",
                      "description": "Detailed view of compensation request",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 456
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "approved",
                            "rejected",
                            "cancelled"
                          ],
                          "example": "pending"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2024-01-20T14:30:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2024-01-20T14:30:00Z"
                        },
                        "effective_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-03-01"
                        },
                        "justification": {
                          "type": "string",
                          "example": "Requesting salary increase based on performance review and market analysis"
                        },
                        "current_compensation": {
                          "type": "object",
                          "properties": {
                            "type": {
                              "type": "string",
                              "example": "salary"
                            },
                            "annual_salary": {
                              "type": "number",
                              "nullable": true,
                              "example": 75000.0
                            },
                            "hourly_rate": {
                              "type": "number",
                              "nullable": true,
                              "example": null
                            },
                            "pay_frequency": {
                              "type": "string",
                              "example": "monthly"
                            },
                            "currency": {
                              "type": "string",
                              "example": "USD"
                            }
                          }
                        },
                        "requested_compensation": {
                          "type": "object",
                          "properties": {
                            "type": {
                              "type": "string",
                              "example": "salary"
                            },
                            "annual_salary": {
                              "type": "number",
                              "nullable": true,
                              "example": 80000.0
                            },
                            "hourly_rate": {
                              "type": "number",
                              "nullable": true,
                              "example": null
                            },
                            "pay_frequency": {
                              "type": "string",
                              "example": "monthly"
                            },
                            "currency": {
                              "type": "string",
                              "example": "USD"
                            }
                          }
                        },
                        "changes": {
                          "type": "object",
                          "properties": {
                            "has_salary_change": {
                              "type": "boolean",
                              "example": true
                            },
                            "has_hourly_change": {
                              "type": "boolean",
                              "example": false
                            },
                            "has_type_change": {
                              "type": "boolean",
                              "example": false
                            },
                            "has_frequency_change": {
                              "type": "boolean",
                              "example": false
                            },
                            "has_currency_change": {
                              "type": "boolean",
                              "example": false
                            },
                            "summary": {
                              "type": "string",
                              "example": "Salary: $75,000 → $80,000"
                            },
                            "impact": {
                              "type": "object",
                              "properties": {
                                "type": {
                                  "type": "string",
                                  "example": "increase"
                                },
                                "amount": {
                                  "type": "number",
                                  "example": 5000.0
                                },
                                "percentage": {
                                  "type": "number",
                                  "example": 6.7
                                },
                                "description": {
                                  "type": "string",
                                  "example": "Increase of $5,000 (6.7%)"
                                }
                              }
                            }
                          }
                        },
                        "approval": {
                          "type": "object",
                          "properties": {
                            "approver": {
                              "type": "object",
                              "nullable": true,
                              "properties": {
                                "id": {
                                  "type": "integer",
                                  "example": 789
                                },
                                "name": {
                                  "type": "string",
                                  "example": "Jane Manager"
                                }
                              }
                            },
                            "approved_at": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true,
                              "example": null
                            },
                            "rejected_at": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true,
                              "example": null
                            },
                            "manager_notes": {
                              "type": "string",
                              "nullable": true,
                              "example": null
                            }
                          }
                        },
                        "status_info": {
                          "type": "object",
                          "properties": {
                            "days_pending": {
                              "type": "integer",
                              "example": 5
                            },
                            "is_urgent": {
                              "type": "boolean",
                              "example": false
                            },
                            "effective_soon": {
                              "type": "boolean",
                              "example": true
                            },
                            "can_edit": {
                              "type": "boolean",
                              "example": true
                            },
                            "can_cancel": {
                              "type": "boolean",
                              "example": true
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests - rate limit exceeded",
            "headers": {
              "Retry-After": {
                "description": "Seconds until rate limit reset",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Limit": {
                "description": "Total requests allowed per window",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests remaining in current window",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/compensation/requests/{id}": {
      "get": {
        "tags": [
          "Compensation Requests"
        ],
        "summary": "Get compensation request details",
        "description": "Retrieves detailed information about a specific compensation change request\nincluding current and requested compensation, approval status, and change analysis.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Compensation request ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Compensation request details retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "compensation_request": {
                      "type": "object",
                      "description": "Detailed view of compensation request",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 456
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "approved",
                            "rejected",
                            "cancelled"
                          ],
                          "example": "pending"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2024-01-20T14:30:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2024-01-20T14:30:00Z"
                        },
                        "effective_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-03-01"
                        },
                        "justification": {
                          "type": "string",
                          "example": "Requesting salary increase based on performance review and market analysis"
                        },
                        "current_compensation": {
                          "type": "object",
                          "properties": {
                            "type": {
                              "type": "string",
                              "example": "salary"
                            },
                            "annual_salary": {
                              "type": "number",
                              "nullable": true,
                              "example": 75000.0
                            },
                            "hourly_rate": {
                              "type": "number",
                              "nullable": true,
                              "example": null
                            },
                            "pay_frequency": {
                              "type": "string",
                              "example": "monthly"
                            },
                            "currency": {
                              "type": "string",
                              "example": "USD"
                            }
                          }
                        },
                        "requested_compensation": {
                          "type": "object",
                          "properties": {
                            "type": {
                              "type": "string",
                              "example": "salary"
                            },
                            "annual_salary": {
                              "type": "number",
                              "nullable": true,
                              "example": 80000.0
                            },
                            "hourly_rate": {
                              "type": "number",
                              "nullable": true,
                              "example": null
                            },
                            "pay_frequency": {
                              "type": "string",
                              "example": "monthly"
                            },
                            "currency": {
                              "type": "string",
                              "example": "USD"
                            }
                          }
                        },
                        "changes": {
                          "type": "object",
                          "properties": {
                            "has_salary_change": {
                              "type": "boolean",
                              "example": true
                            },
                            "has_hourly_change": {
                              "type": "boolean",
                              "example": false
                            },
                            "has_type_change": {
                              "type": "boolean",
                              "example": false
                            },
                            "has_frequency_change": {
                              "type": "boolean",
                              "example": false
                            },
                            "has_currency_change": {
                              "type": "boolean",
                              "example": false
                            },
                            "summary": {
                              "type": "string",
                              "example": "Salary: $75,000 → $80,000"
                            },
                            "impact": {
                              "type": "object",
                              "properties": {
                                "type": {
                                  "type": "string",
                                  "example": "increase"
                                },
                                "amount": {
                                  "type": "number",
                                  "example": 5000.0
                                },
                                "percentage": {
                                  "type": "number",
                                  "example": 6.7
                                },
                                "description": {
                                  "type": "string",
                                  "example": "Increase of $5,000 (6.7%)"
                                }
                              }
                            }
                          }
                        },
                        "approval": {
                          "type": "object",
                          "properties": {
                            "approver": {
                              "type": "object",
                              "nullable": true,
                              "properties": {
                                "id": {
                                  "type": "integer",
                                  "example": 789
                                },
                                "name": {
                                  "type": "string",
                                  "example": "Jane Manager"
                                }
                              }
                            },
                            "approved_at": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true,
                              "example": null
                            },
                            "rejected_at": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true,
                              "example": null
                            },
                            "manager_notes": {
                              "type": "string",
                              "nullable": true,
                              "example": null
                            }
                          }
                        },
                        "status_info": {
                          "type": "object",
                          "properties": {
                            "days_pending": {
                              "type": "integer",
                              "example": 5
                            },
                            "is_urgent": {
                              "type": "boolean",
                              "example": false
                            },
                            "effective_soon": {
                              "type": "boolean",
                              "example": true
                            },
                            "can_edit": {
                              "type": "boolean",
                              "example": true
                            },
                            "can_cancel": {
                              "type": "boolean",
                              "example": true
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Compensation Requests"
        ],
        "summary": "Update compensation request",
        "description": "Updates a pending compensation change request. Only pending requests can be edited.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Compensation request ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "compensation_request"
                ],
                "properties": {
                  "compensation_request": {
                    "type": "object",
                    "description": "Input schema for creating/updating compensation requests",
                    "required": [
                      "justification",
                      "requested_compensation_type"
                    ],
                    "properties": {
                      "justification": {
                        "type": "string",
                        "minLength": 10,
                        "maxLength": 1000,
                        "example": "Requesting salary increase based on performance review and market analysis"
                      },
                      "effective_date": {
                        "type": "string",
                        "format": "date",
                        "example": "2024-03-01"
                      },
                      "requested_compensation_type": {
                        "type": "string",
                        "enum": [
                          "salary",
                          "hourly",
                          "commission",
                          "contract"
                        ],
                        "example": "salary"
                      },
                      "requested_annual_salary": {
                        "type": "number",
                        "minimum": 0,
                        "nullable": true,
                        "example": 80000.0
                      },
                      "requested_hourly_rate": {
                        "type": "number",
                        "minimum": 0,
                        "nullable": true,
                        "example": null
                      },
                      "requested_pay_frequency": {
                        "type": "string",
                        "enum": [
                          "weekly",
                          "biweekly",
                          "monthly",
                          "quarterly",
                          "annually"
                        ],
                        "example": "monthly"
                      },
                      "requested_currency": {
                        "type": "string",
                        "example": "USD"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Compensation request updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "compensation_request": {
                      "type": "object",
                      "description": "Detailed view of compensation request",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 456
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "approved",
                            "rejected",
                            "cancelled"
                          ],
                          "example": "pending"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2024-01-20T14:30:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2024-01-20T14:30:00Z"
                        },
                        "effective_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-03-01"
                        },
                        "justification": {
                          "type": "string",
                          "example": "Requesting salary increase based on performance review and market analysis"
                        },
                        "current_compensation": {
                          "type": "object",
                          "properties": {
                            "type": {
                              "type": "string",
                              "example": "salary"
                            },
                            "annual_salary": {
                              "type": "number",
                              "nullable": true,
                              "example": 75000.0
                            },
                            "hourly_rate": {
                              "type": "number",
                              "nullable": true,
                              "example": null
                            },
                            "pay_frequency": {
                              "type": "string",
                              "example": "monthly"
                            },
                            "currency": {
                              "type": "string",
                              "example": "USD"
                            }
                          }
                        },
                        "requested_compensation": {
                          "type": "object",
                          "properties": {
                            "type": {
                              "type": "string",
                              "example": "salary"
                            },
                            "annual_salary": {
                              "type": "number",
                              "nullable": true,
                              "example": 80000.0
                            },
                            "hourly_rate": {
                              "type": "number",
                              "nullable": true,
                              "example": null
                            },
                            "pay_frequency": {
                              "type": "string",
                              "example": "monthly"
                            },
                            "currency": {
                              "type": "string",
                              "example": "USD"
                            }
                          }
                        },
                        "changes": {
                          "type": "object",
                          "properties": {
                            "has_salary_change": {
                              "type": "boolean",
                              "example": true
                            },
                            "has_hourly_change": {
                              "type": "boolean",
                              "example": false
                            },
                            "has_type_change": {
                              "type": "boolean",
                              "example": false
                            },
                            "has_frequency_change": {
                              "type": "boolean",
                              "example": false
                            },
                            "has_currency_change": {
                              "type": "boolean",
                              "example": false
                            },
                            "summary": {
                              "type": "string",
                              "example": "Salary: $75,000 → $80,000"
                            },
                            "impact": {
                              "type": "object",
                              "properties": {
                                "type": {
                                  "type": "string",
                                  "example": "increase"
                                },
                                "amount": {
                                  "type": "number",
                                  "example": 5000.0
                                },
                                "percentage": {
                                  "type": "number",
                                  "example": 6.7
                                },
                                "description": {
                                  "type": "string",
                                  "example": "Increase of $5,000 (6.7%)"
                                }
                              }
                            }
                          }
                        },
                        "approval": {
                          "type": "object",
                          "properties": {
                            "approver": {
                              "type": "object",
                              "nullable": true,
                              "properties": {
                                "id": {
                                  "type": "integer",
                                  "example": 789
                                },
                                "name": {
                                  "type": "string",
                                  "example": "Jane Manager"
                                }
                              }
                            },
                            "approved_at": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true,
                              "example": null
                            },
                            "rejected_at": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true,
                              "example": null
                            },
                            "manager_notes": {
                              "type": "string",
                              "nullable": true,
                              "example": null
                            }
                          }
                        },
                        "status_info": {
                          "type": "object",
                          "properties": {
                            "days_pending": {
                              "type": "integer",
                              "example": 5
                            },
                            "is_urgent": {
                              "type": "boolean",
                              "example": false
                            },
                            "effective_soon": {
                              "type": "boolean",
                              "example": true
                            },
                            "can_edit": {
                              "type": "boolean",
                              "example": true
                            },
                            "can_cancel": {
                              "type": "boolean",
                              "example": true
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          }
        }
      }
    },
    "/compensation/requests/{id}/cancel": {
      "patch": {
        "tags": [
          "Compensation Requests"
        ],
        "summary": "Cancel compensation request",
        "description": "Cancels a pending compensation change request. Only pending requests can be cancelled.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Compensation request ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Compensation request cancelled successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "compensation_request": {
                      "type": "object",
                      "description": "Detailed view of compensation request",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 456
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "approved",
                            "rejected",
                            "cancelled"
                          ],
                          "example": "pending"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2024-01-20T14:30:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2024-01-20T14:30:00Z"
                        },
                        "effective_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-03-01"
                        },
                        "justification": {
                          "type": "string",
                          "example": "Requesting salary increase based on performance review and market analysis"
                        },
                        "current_compensation": {
                          "type": "object",
                          "properties": {
                            "type": {
                              "type": "string",
                              "example": "salary"
                            },
                            "annual_salary": {
                              "type": "number",
                              "nullable": true,
                              "example": 75000.0
                            },
                            "hourly_rate": {
                              "type": "number",
                              "nullable": true,
                              "example": null
                            },
                            "pay_frequency": {
                              "type": "string",
                              "example": "monthly"
                            },
                            "currency": {
                              "type": "string",
                              "example": "USD"
                            }
                          }
                        },
                        "requested_compensation": {
                          "type": "object",
                          "properties": {
                            "type": {
                              "type": "string",
                              "example": "salary"
                            },
                            "annual_salary": {
                              "type": "number",
                              "nullable": true,
                              "example": 80000.0
                            },
                            "hourly_rate": {
                              "type": "number",
                              "nullable": true,
                              "example": null
                            },
                            "pay_frequency": {
                              "type": "string",
                              "example": "monthly"
                            },
                            "currency": {
                              "type": "string",
                              "example": "USD"
                            }
                          }
                        },
                        "changes": {
                          "type": "object",
                          "properties": {
                            "has_salary_change": {
                              "type": "boolean",
                              "example": true
                            },
                            "has_hourly_change": {
                              "type": "boolean",
                              "example": false
                            },
                            "has_type_change": {
                              "type": "boolean",
                              "example": false
                            },
                            "has_frequency_change": {
                              "type": "boolean",
                              "example": false
                            },
                            "has_currency_change": {
                              "type": "boolean",
                              "example": false
                            },
                            "summary": {
                              "type": "string",
                              "example": "Salary: $75,000 → $80,000"
                            },
                            "impact": {
                              "type": "object",
                              "properties": {
                                "type": {
                                  "type": "string",
                                  "example": "increase"
                                },
                                "amount": {
                                  "type": "number",
                                  "example": 5000.0
                                },
                                "percentage": {
                                  "type": "number",
                                  "example": 6.7
                                },
                                "description": {
                                  "type": "string",
                                  "example": "Increase of $5,000 (6.7%)"
                                }
                              }
                            }
                          }
                        },
                        "approval": {
                          "type": "object",
                          "properties": {
                            "approver": {
                              "type": "object",
                              "nullable": true,
                              "properties": {
                                "id": {
                                  "type": "integer",
                                  "example": 789
                                },
                                "name": {
                                  "type": "string",
                                  "example": "Jane Manager"
                                }
                              }
                            },
                            "approved_at": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true,
                              "example": null
                            },
                            "rejected_at": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true,
                              "example": null
                            },
                            "manager_notes": {
                              "type": "string",
                              "nullable": true,
                              "example": null
                            }
                          }
                        },
                        "status_info": {
                          "type": "object",
                          "properties": {
                            "days_pending": {
                              "type": "integer",
                              "example": 5
                            },
                            "is_urgent": {
                              "type": "boolean",
                              "example": false
                            },
                            "effective_soon": {
                              "type": "boolean",
                              "example": true
                            },
                            "can_edit": {
                              "type": "boolean",
                              "example": true
                            },
                            "can_cancel": {
                              "type": "boolean",
                              "example": true
                            }
                          }
                        }
                      }
                    },
                    "message": {
                      "type": "string",
                      "example": "Compensation request has been cancelled successfully"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Request cannot be cancelled (not pending)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/epms/dashboard": {
      "get": {
        "tags": [
          "EPMS Dashboard"
        ],
        "summary": "Get EPMS dashboard",
        "description": "Retrieves combined dashboard data for the current user including goals, reviews, feedback, meetings, and action items.\nProvides an aggregated view of all EPMS activities and pending actions.\n\n**Required Scopes:** `read:epms_dashboard`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Dashboard data retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "dashboard": {
                      "type": "object",
                      "description": "EPMS dashboard data with aggregated goals, reviews, feedback, and meetings",
                      "properties": {
                        "goals": {
                          "type": "object",
                          "properties": {
                            "active_count": {
                              "type": "integer",
                              "example": 5
                            },
                            "completed_count": {
                              "type": "integer",
                              "example": 12
                            },
                            "overdue_count": {
                              "type": "integer",
                              "example": 2
                            },
                            "recent_updates": {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/EPMSGoal"
                              }
                            }
                          }
                        },
                        "reviews": {
                          "type": "object",
                          "properties": {
                            "pending_count": {
                              "type": "integer",
                              "example": 1
                            },
                            "in_progress_count": {
                              "type": "integer",
                              "example": 2
                            },
                            "completed_count": {
                              "type": "integer",
                              "example": 8
                            },
                            "recent_reviews": {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/EPMSPerformanceReview"
                              }
                            }
                          }
                        },
                        "feedback": {
                          "type": "object",
                          "properties": {
                            "received_count": {
                              "type": "integer",
                              "example": 15
                            },
                            "pending_acknowledgment": {
                              "type": "integer",
                              "example": 3
                            },
                            "recent_feedback": {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/EPMSContinuousFeedback"
                              }
                            }
                          }
                        },
                        "meetings": {
                          "type": "object",
                          "properties": {
                            "upcoming_count": {
                              "type": "integer",
                              "example": 4
                            },
                            "recent_count": {
                              "type": "integer",
                              "example": 6
                            },
                            "recent_meetings": {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/EPMSMeeting"
                              }
                            }
                          }
                        },
                        "action_items": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "type": {
                                "type": "string",
                                "enum": [
                                  "goal_update",
                                  "review_submit",
                                  "feedback_acknowledge",
                                  "meeting_schedule"
                                ]
                              },
                              "title": {
                                "type": "string"
                              },
                              "due_date": {
                                "type": "string",
                                "format": "date",
                                "nullable": true
                              },
                              "priority": {
                                "type": "string",
                                "enum": [
                                  "low",
                                  "medium",
                                  "high",
                                  "critical"
                                ]
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/dashboard/team": {
      "get": {
        "tags": [
          "EPMS Dashboard"
        ],
        "summary": "Get team dashboard",
        "description": "Retrieves team dashboard data for managers. Includes team goals, reviews, feedback, and pending approvals.\nRequires manager permissions.\n\n**Required Scopes:** `read:epms_dashboard`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Team dashboard data retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "dashboard": {
                      "type": "object",
                      "description": "Team dashboard data for managers",
                      "properties": {
                        "team_stats": {
                          "type": "object",
                          "properties": {
                            "team_size": {
                              "type": "integer",
                              "example": 12
                            },
                            "active_goals": {
                              "type": "integer",
                              "example": 45
                            },
                            "pending_reviews": {
                              "type": "integer",
                              "example": 8
                            },
                            "pending_approvals": {
                              "type": "integer",
                              "example": 5
                            }
                          }
                        },
                        "team_goals": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/EPMSGoal"
                          }
                        },
                        "pending_reviews": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/EPMSPerformanceReview"
                          }
                        },
                        "team_feedback": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/EPMSContinuousFeedback"
                          }
                        },
                        "upcoming_meetings": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/EPMSMeeting"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/goals": {
      "get": {
        "tags": [
          "EPMS Goals"
        ],
        "summary": "List goals",
        "description": "Retrieves a list of goals with filtering and pagination options. Supports filtering by scope, status, type, priority, and employee.\n\n**Required Scopes:** `read:epms_goals`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "description": "Number of items per page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "scope",
            "in": "query",
            "description": "Filter by scope (mine, team, all)",
            "schema": {
              "type": "string",
              "enum": [
                "mine",
                "team",
                "all"
              ]
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by status (comma-separated: draft, in_review, active, on_hold, completed, cancelled, overdue)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "goal_type",
            "in": "query",
            "description": "Filter by goal type",
            "schema": {
              "type": "string",
              "enum": [
                "performance",
                "development",
                "behavior",
                "project",
                "skill",
                "stretch",
                "team"
              ]
            }
          },
          {
            "name": "priority",
            "in": "query",
            "description": "Filter by priority",
            "schema": {
              "type": "string",
              "enum": [
                "low",
                "medium",
                "high",
                "critical"
              ]
            }
          },
          {
            "name": "employee_id",
            "in": "query",
            "description": "Filter by employee ID",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "overdue",
            "in": "query",
            "description": "Filter overdue goals",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Search by title or description",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "workflow_stage",
            "in": "query",
            "description": "Filter by workflow stage (comma-separated: draft, employee_review, manager_finalized, leadership_approved)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "start_date",
            "in": "query",
            "description": "Start of target_date range (inclusive)",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "end_date",
            "in": "query",
            "description": "End of target_date range (inclusive)",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "due_within_days",
            "in": "query",
            "description": "Filter goals due within N days",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "performance_review_id",
            "in": "query",
            "description": "Filter by linked performance review ID",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "sort_by",
            "in": "query",
            "description": "Sort field",
            "schema": {
              "type": "string",
              "enum": [
                "created_at",
                "updated_at",
                "target_date",
                "start_date",
                "progress_percentage",
                "priority",
                "status",
                "title"
              ]
            }
          },
          {
            "name": "sort_order",
            "in": "query",
            "description": "Sort direction",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Goals retrieved successfully",
            "headers": {
              "X-Total-Count": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Total-Pages": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Current-Page": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Per-Page": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "Employee goal with progress tracking. Progress updates are returned as a sibling array in GET /goals/{id}, not nested in the goal object.",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 123
                          },
                          "title": {
                            "type": "string",
                            "example": "Increase sales by 20%"
                          },
                          "description": {
                            "type": "string",
                            "nullable": true,
                            "example": "Achieve 20% growth in Q1 sales"
                          },
                          "goal_type": {
                            "type": "string",
                            "enum": [
                              "performance",
                              "development",
                              "behavior",
                              "project",
                              "skill"
                            ],
                            "example": "performance"
                          },
                          "goal_type_label": {
                            "type": "string",
                            "nullable": true,
                            "description": "Human-readable label for goal type"
                          },
                          "goal_category": {
                            "type": "string",
                            "nullable": true,
                            "description": "The focus area category key for this goal",
                            "example": "professional_development"
                          },
                          "goal_category_label": {
                            "type": "string",
                            "nullable": true,
                            "description": "Human-readable label for the goal category",
                            "example": "Professional Development"
                          },
                          "priority": {
                            "type": "string",
                            "enum": [
                              "low",
                              "medium",
                              "high",
                              "critical"
                            ],
                            "example": "high"
                          },
                          "priority_label": {
                            "type": "string",
                            "nullable": true,
                            "description": "Human-readable label for priority"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "draft",
                              "in_review",
                              "active",
                              "on_hold",
                              "completed",
                              "cancelled",
                              "overdue"
                            ],
                            "example": "active"
                          },
                          "display_status": {
                            "type": "string",
                            "nullable": true,
                            "description": "Human-readable display status"
                          },
                          "start_date": {
                            "type": "string",
                            "format": "date",
                            "example": "2026-01-01"
                          },
                          "target_date": {
                            "type": "string",
                            "format": "date",
                            "example": "2026-03-31"
                          },
                          "completed_date": {
                            "type": "string",
                            "format": "date",
                            "nullable": true
                          },
                          "progress_percentage": {
                            "type": "number",
                            "minimum": 0,
                            "maximum": 100,
                            "example": 45.5
                          },
                          "weight_percentage": {
                            "type": "number",
                            "nullable": true,
                            "example": 30.0
                          },
                          "workflow_stage": {
                            "type": "string",
                            "enum": [
                              "draft",
                              "employee_review",
                              "manager_finalized",
                              "leadership_approved"
                            ],
                            "description": "Current workflow stage"
                          },
                          "workflow_stage_label": {
                            "type": "string",
                            "nullable": true
                          },
                          "is_smart_goal": {
                            "type": "boolean",
                            "example": true
                          },
                          "smart_score": {
                            "type": "number",
                            "nullable": true
                          },
                          "on_track": {
                            "type": "boolean",
                            "nullable": true
                          },
                          "days_until_due": {
                            "type": "integer",
                            "nullable": true
                          },
                          "is_overdue": {
                            "type": "boolean",
                            "example": false
                          },
                          "progress_update_allowed": {
                            "type": "boolean",
                            "description": "Whether progress updates can be submitted for this goal"
                          },
                          "success_criteria": {
                            "type": "string",
                            "nullable": true,
                            "description": "Present when include_details is true (e.g. show endpoint)",
                            "example": "Reach $500K in sales"
                          },
                          "smart_criteria": {
                            "type": "object",
                            "nullable": true,
                            "description": "Present when include_details is true",
                            "properties": {
                              "is_specific": {
                                "type": "boolean"
                              },
                              "is_measurable": {
                                "type": "boolean"
                              },
                              "is_achievable": {
                                "type": "boolean"
                              },
                              "is_relevant": {
                                "type": "boolean"
                              },
                              "is_time_bound": {
                                "type": "boolean"
                              }
                            }
                          },
                          "progress_updates_count": {
                            "type": "integer",
                            "description": "Present when include_details is true"
                          },
                          "latest_progress_update": {
                            "$ref": "#/components/schemas/EPMSProgressUpdate",
                            "nullable": true,
                            "description": "Present when include_details is true"
                          },
                          "can_edit": {
                            "type": "boolean",
                            "description": "Present when include_details is true"
                          },
                          "can_complete": {
                            "type": "boolean",
                            "description": "Present when include_details is true"
                          },
                          "can_delete": {
                            "type": "boolean",
                            "description": "Present when include_details is true"
                          },
                          "requires_manager_approval": {
                            "type": "boolean",
                            "description": "Present when include_details is true"
                          },
                          "is_fully_approved": {
                            "type": "boolean",
                            "description": "Present when include_details is true"
                          },
                          "in_review_stage": {
                            "type": "boolean",
                            "description": "Present when include_details is true"
                          },
                          "can_cancel": {
                            "type": "boolean",
                            "description": "Present when include_details is true"
                          },
                          "can_put_on_hold": {
                            "type": "boolean",
                            "description": "Present when include_details is true"
                          },
                          "can_reactivate": {
                            "type": "boolean",
                            "description": "Present when include_details is true"
                          },
                          "can_send_to_employee": {
                            "type": "boolean",
                            "description": "Present when include_details is true"
                          },
                          "can_employee_review": {
                            "type": "boolean",
                            "description": "Present when include_details is true"
                          },
                          "can_manager_finalize": {
                            "type": "boolean",
                            "description": "Present when include_details is true"
                          },
                          "can_leadership_approve": {
                            "type": "boolean",
                            "description": "Present when include_details is true"
                          },
                          "manager_approved_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "description": "Present when include_details is true"
                          },
                          "leadership_approved_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "description": "Present when include_details is true"
                          },
                          "employee": {
                            "type": "object",
                            "nullable": true,
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 456
                              },
                              "name": {
                                "type": "string",
                                "example": "John Doe"
                              },
                              "email": {
                                "type": "string",
                                "nullable": true,
                                "example": "john@example.com"
                              },
                              "job_title": {
                                "type": "string",
                                "nullable": true
                              }
                            }
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2026-01-01T10:00:00Z"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2026-01-15T14:30:00Z"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 6
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Items per page",
                          "example": 25
                        }
                      },
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid filter parameter (e.g. invalid workflow_stage or sort_by value)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "object",
                          "properties": {
                            "param": {
                              "type": "string"
                            },
                            "invalid_values": {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            },
                            "valid_values": {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "EPMS Goals"
        ],
        "summary": "Create goal",
        "description": "Creates a new goal. Employee ID defaults to current user if not specified.\nValidates goal attributes including SMART criteria.\n\n`goal_category` is required when creating a goal and must be a valid category key for the business;\nuse `GET /epms/goals/categories` to retrieve valid values.\n\n**Required Scopes:** `write:epms_goals`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "goal"
                ],
                "properties": {
                  "goal": {
                    "type": "object",
                    "description": "Input schema for creating/updating goals",
                    "required": [
                      "title"
                    ],
                    "properties": {
                      "title": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 255,
                        "example": "Increase sales by 20%"
                      },
                      "description": {
                        "type": "string",
                        "maxLength": 2000,
                        "example": "Achieve 20% growth in Q1 sales"
                      },
                      "goal_type": {
                        "type": "string",
                        "enum": [
                          "performance",
                          "development",
                          "behavior",
                          "project",
                          "skill"
                        ],
                        "example": "performance"
                      },
                      "goal_category": {
                        "type": "string",
                        "maxLength": 50,
                        "description": "Focus area category key. Required on create for non-department goals. Must be a valid category configured for the business. Use `GET /epms/goals/categories` to retrieve valid values. Default categories for IC: professional_development, lead_self, work_with_others, contribute_to_business. Default categories for leaders: professional_development, lead_self, lead_others, lead_the_business. Business configuration may differ.\n",
                        "example": "professional_development"
                      },
                      "priority": {
                        "type": "string",
                        "enum": [
                          "low",
                          "medium",
                          "high",
                          "critical"
                        ],
                        "example": "high"
                      },
                      "start_date": {
                        "type": "string",
                        "format": "date",
                        "example": "2026-01-01"
                      },
                      "target_date": {
                        "type": "string",
                        "format": "date",
                        "example": "2026-03-31"
                      },
                      "progress_percentage": {
                        "type": "number",
                        "minimum": 0,
                        "maximum": 100,
                        "example": 0
                      },
                      "weight_percentage": {
                        "type": "number",
                        "minimum": 0,
                        "maximum": 100,
                        "example": 30.0
                      },
                      "success_criteria": {
                        "type": "string",
                        "maxLength": 500,
                        "example": "Reach $500K in sales"
                      },
                      "employee_id": {
                        "type": "integer",
                        "example": 456
                      },
                      "is_specific": {
                        "type": "boolean",
                        "example": true
                      },
                      "is_measurable": {
                        "type": "boolean",
                        "example": true
                      },
                      "is_achievable": {
                        "type": "boolean",
                        "example": true
                      },
                      "is_relevant": {
                        "type": "boolean",
                        "example": true
                      },
                      "is_time_bound": {
                        "type": "boolean",
                        "example": true
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Goal created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "goal": {
                      "type": "object",
                      "description": "Employee goal with progress tracking. Progress updates are returned as a sibling array in GET /goals/{id}, not nested in the goal object.",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 123
                        },
                        "title": {
                          "type": "string",
                          "example": "Increase sales by 20%"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "example": "Achieve 20% growth in Q1 sales"
                        },
                        "goal_type": {
                          "type": "string",
                          "enum": [
                            "performance",
                            "development",
                            "behavior",
                            "project",
                            "skill"
                          ],
                          "example": "performance"
                        },
                        "goal_type_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for goal type"
                        },
                        "goal_category": {
                          "type": "string",
                          "nullable": true,
                          "description": "The focus area category key for this goal",
                          "example": "professional_development"
                        },
                        "goal_category_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for the goal category",
                          "example": "Professional Development"
                        },
                        "priority": {
                          "type": "string",
                          "enum": [
                            "low",
                            "medium",
                            "high",
                            "critical"
                          ],
                          "example": "high"
                        },
                        "priority_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for priority"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "in_review",
                            "active",
                            "on_hold",
                            "completed",
                            "cancelled",
                            "overdue"
                          ],
                          "example": "active"
                        },
                        "display_status": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable display status"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-01"
                        },
                        "target_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-03-31"
                        },
                        "completed_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "progress_percentage": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 100,
                          "example": 45.5
                        },
                        "weight_percentage": {
                          "type": "number",
                          "nullable": true,
                          "example": 30.0
                        },
                        "workflow_stage": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "employee_review",
                            "manager_finalized",
                            "leadership_approved"
                          ],
                          "description": "Current workflow stage"
                        },
                        "workflow_stage_label": {
                          "type": "string",
                          "nullable": true
                        },
                        "is_smart_goal": {
                          "type": "boolean",
                          "example": true
                        },
                        "smart_score": {
                          "type": "number",
                          "nullable": true
                        },
                        "on_track": {
                          "type": "boolean",
                          "nullable": true
                        },
                        "days_until_due": {
                          "type": "integer",
                          "nullable": true
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        },
                        "progress_update_allowed": {
                          "type": "boolean",
                          "description": "Whether progress updates can be submitted for this goal"
                        },
                        "success_criteria": {
                          "type": "string",
                          "nullable": true,
                          "description": "Present when include_details is true (e.g. show endpoint)",
                          "example": "Reach $500K in sales"
                        },
                        "smart_criteria": {
                          "type": "object",
                          "nullable": true,
                          "description": "Present when include_details is true",
                          "properties": {
                            "is_specific": {
                              "type": "boolean"
                            },
                            "is_measurable": {
                              "type": "boolean"
                            },
                            "is_achievable": {
                              "type": "boolean"
                            },
                            "is_relevant": {
                              "type": "boolean"
                            },
                            "is_time_bound": {
                              "type": "boolean"
                            }
                          }
                        },
                        "progress_updates_count": {
                          "type": "integer",
                          "description": "Present when include_details is true"
                        },
                        "latest_progress_update": {
                          "$ref": "#/components/schemas/EPMSProgressUpdate",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_complete": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_delete": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "requires_manager_approval": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "is_fully_approved": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "in_review_stage": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_cancel": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_put_on_hold": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_reactivate": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_send_to_employee": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_employee_review": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_manager_finalize": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_leadership_approve": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "manager_approved_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "leadership_approved_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "employee": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "john@example.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-01T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/goals/{id}": {
      "get": {
        "tags": [
          "EPMS Goals"
        ],
        "summary": "Get goal details",
        "description": "Retrieves detailed goal information including progress updates and related data.\n\n**Required Scopes:** `read:epms_goals`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Goal ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Goal details retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "goal": {
                      "type": "object",
                      "description": "Employee goal with progress tracking. Progress updates are returned as a sibling array in GET /goals/{id}, not nested in the goal object.",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 123
                        },
                        "title": {
                          "type": "string",
                          "example": "Increase sales by 20%"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "example": "Achieve 20% growth in Q1 sales"
                        },
                        "goal_type": {
                          "type": "string",
                          "enum": [
                            "performance",
                            "development",
                            "behavior",
                            "project",
                            "skill"
                          ],
                          "example": "performance"
                        },
                        "goal_type_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for goal type"
                        },
                        "goal_category": {
                          "type": "string",
                          "nullable": true,
                          "description": "The focus area category key for this goal",
                          "example": "professional_development"
                        },
                        "goal_category_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for the goal category",
                          "example": "Professional Development"
                        },
                        "priority": {
                          "type": "string",
                          "enum": [
                            "low",
                            "medium",
                            "high",
                            "critical"
                          ],
                          "example": "high"
                        },
                        "priority_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for priority"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "in_review",
                            "active",
                            "on_hold",
                            "completed",
                            "cancelled",
                            "overdue"
                          ],
                          "example": "active"
                        },
                        "display_status": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable display status"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-01"
                        },
                        "target_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-03-31"
                        },
                        "completed_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "progress_percentage": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 100,
                          "example": 45.5
                        },
                        "weight_percentage": {
                          "type": "number",
                          "nullable": true,
                          "example": 30.0
                        },
                        "workflow_stage": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "employee_review",
                            "manager_finalized",
                            "leadership_approved"
                          ],
                          "description": "Current workflow stage"
                        },
                        "workflow_stage_label": {
                          "type": "string",
                          "nullable": true
                        },
                        "is_smart_goal": {
                          "type": "boolean",
                          "example": true
                        },
                        "smart_score": {
                          "type": "number",
                          "nullable": true
                        },
                        "on_track": {
                          "type": "boolean",
                          "nullable": true
                        },
                        "days_until_due": {
                          "type": "integer",
                          "nullable": true
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        },
                        "progress_update_allowed": {
                          "type": "boolean",
                          "description": "Whether progress updates can be submitted for this goal"
                        },
                        "success_criteria": {
                          "type": "string",
                          "nullable": true,
                          "description": "Present when include_details is true (e.g. show endpoint)",
                          "example": "Reach $500K in sales"
                        },
                        "smart_criteria": {
                          "type": "object",
                          "nullable": true,
                          "description": "Present when include_details is true",
                          "properties": {
                            "is_specific": {
                              "type": "boolean"
                            },
                            "is_measurable": {
                              "type": "boolean"
                            },
                            "is_achievable": {
                              "type": "boolean"
                            },
                            "is_relevant": {
                              "type": "boolean"
                            },
                            "is_time_bound": {
                              "type": "boolean"
                            }
                          }
                        },
                        "progress_updates_count": {
                          "type": "integer",
                          "description": "Present when include_details is true"
                        },
                        "latest_progress_update": {
                          "$ref": "#/components/schemas/EPMSProgressUpdate",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_complete": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_delete": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "requires_manager_approval": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "is_fully_approved": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "in_review_stage": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_cancel": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_put_on_hold": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_reactivate": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_send_to_employee": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_employee_review": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_manager_finalize": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_leadership_approve": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "manager_approved_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "leadership_approved_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "employee": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "john@example.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-01T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        }
                      }
                    },
                    "progress_updates": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "Goal progress update record",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 789
                          },
                          "progress_percentage": {
                            "type": "number",
                            "minimum": 0,
                            "maximum": 100,
                            "example": 50.0
                          },
                          "update_notes": {
                            "type": "string",
                            "nullable": true,
                            "example": "Halfway through the quarter, on track"
                          },
                          "update_date": {
                            "type": "string",
                            "format": "date",
                            "example": "2026-01-15"
                          },
                          "update_type": {
                            "type": "string",
                            "enum": [
                              "regular",
                              "milestone",
                              "completion",
                              "revision",
                              "comment"
                            ],
                            "example": "regular"
                          },
                          "challenges_faced": {
                            "type": "string",
                            "nullable": true
                          },
                          "support_needed": {
                            "type": "string",
                            "nullable": true
                          },
                          "attachments": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            },
                            "nullable": true
                          },
                          "updated_by": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 456
                              },
                              "name": {
                                "type": "string",
                                "example": "John Doe"
                              }
                            }
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2026-01-15T10:30:00Z"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "EPMS Goals"
        ],
        "summary": "Update goal",
        "description": "Updates an existing goal. Only certain fields can be updated based on goal status.\n\n`goal_category` can be included in the body to update the goal's focus area (same validation as create).\n\n**Required Scopes:** `write:epms_goals`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Goal ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "goal"
                ],
                "properties": {
                  "goal": {
                    "type": "object",
                    "description": "Input schema for creating/updating goals",
                    "required": [
                      "title"
                    ],
                    "properties": {
                      "title": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 255,
                        "example": "Increase sales by 20%"
                      },
                      "description": {
                        "type": "string",
                        "maxLength": 2000,
                        "example": "Achieve 20% growth in Q1 sales"
                      },
                      "goal_type": {
                        "type": "string",
                        "enum": [
                          "performance",
                          "development",
                          "behavior",
                          "project",
                          "skill"
                        ],
                        "example": "performance"
                      },
                      "goal_category": {
                        "type": "string",
                        "maxLength": 50,
                        "description": "Focus area category key. Required on create for non-department goals. Must be a valid category configured for the business. Use `GET /epms/goals/categories` to retrieve valid values. Default categories for IC: professional_development, lead_self, work_with_others, contribute_to_business. Default categories for leaders: professional_development, lead_self, lead_others, lead_the_business. Business configuration may differ.\n",
                        "example": "professional_development"
                      },
                      "priority": {
                        "type": "string",
                        "enum": [
                          "low",
                          "medium",
                          "high",
                          "critical"
                        ],
                        "example": "high"
                      },
                      "start_date": {
                        "type": "string",
                        "format": "date",
                        "example": "2026-01-01"
                      },
                      "target_date": {
                        "type": "string",
                        "format": "date",
                        "example": "2026-03-31"
                      },
                      "progress_percentage": {
                        "type": "number",
                        "minimum": 0,
                        "maximum": 100,
                        "example": 0
                      },
                      "weight_percentage": {
                        "type": "number",
                        "minimum": 0,
                        "maximum": 100,
                        "example": 30.0
                      },
                      "success_criteria": {
                        "type": "string",
                        "maxLength": 500,
                        "example": "Reach $500K in sales"
                      },
                      "employee_id": {
                        "type": "integer",
                        "example": 456
                      },
                      "is_specific": {
                        "type": "boolean",
                        "example": true
                      },
                      "is_measurable": {
                        "type": "boolean",
                        "example": true
                      },
                      "is_achievable": {
                        "type": "boolean",
                        "example": true
                      },
                      "is_relevant": {
                        "type": "boolean",
                        "example": true
                      },
                      "is_time_bound": {
                        "type": "boolean",
                        "example": true
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Goal updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "goal": {
                      "type": "object",
                      "description": "Employee goal with progress tracking. Progress updates are returned as a sibling array in GET /goals/{id}, not nested in the goal object.",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 123
                        },
                        "title": {
                          "type": "string",
                          "example": "Increase sales by 20%"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "example": "Achieve 20% growth in Q1 sales"
                        },
                        "goal_type": {
                          "type": "string",
                          "enum": [
                            "performance",
                            "development",
                            "behavior",
                            "project",
                            "skill"
                          ],
                          "example": "performance"
                        },
                        "goal_type_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for goal type"
                        },
                        "goal_category": {
                          "type": "string",
                          "nullable": true,
                          "description": "The focus area category key for this goal",
                          "example": "professional_development"
                        },
                        "goal_category_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for the goal category",
                          "example": "Professional Development"
                        },
                        "priority": {
                          "type": "string",
                          "enum": [
                            "low",
                            "medium",
                            "high",
                            "critical"
                          ],
                          "example": "high"
                        },
                        "priority_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for priority"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "in_review",
                            "active",
                            "on_hold",
                            "completed",
                            "cancelled",
                            "overdue"
                          ],
                          "example": "active"
                        },
                        "display_status": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable display status"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-01"
                        },
                        "target_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-03-31"
                        },
                        "completed_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "progress_percentage": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 100,
                          "example": 45.5
                        },
                        "weight_percentage": {
                          "type": "number",
                          "nullable": true,
                          "example": 30.0
                        },
                        "workflow_stage": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "employee_review",
                            "manager_finalized",
                            "leadership_approved"
                          ],
                          "description": "Current workflow stage"
                        },
                        "workflow_stage_label": {
                          "type": "string",
                          "nullable": true
                        },
                        "is_smart_goal": {
                          "type": "boolean",
                          "example": true
                        },
                        "smart_score": {
                          "type": "number",
                          "nullable": true
                        },
                        "on_track": {
                          "type": "boolean",
                          "nullable": true
                        },
                        "days_until_due": {
                          "type": "integer",
                          "nullable": true
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        },
                        "progress_update_allowed": {
                          "type": "boolean",
                          "description": "Whether progress updates can be submitted for this goal"
                        },
                        "success_criteria": {
                          "type": "string",
                          "nullable": true,
                          "description": "Present when include_details is true (e.g. show endpoint)",
                          "example": "Reach $500K in sales"
                        },
                        "smart_criteria": {
                          "type": "object",
                          "nullable": true,
                          "description": "Present when include_details is true",
                          "properties": {
                            "is_specific": {
                              "type": "boolean"
                            },
                            "is_measurable": {
                              "type": "boolean"
                            },
                            "is_achievable": {
                              "type": "boolean"
                            },
                            "is_relevant": {
                              "type": "boolean"
                            },
                            "is_time_bound": {
                              "type": "boolean"
                            }
                          }
                        },
                        "progress_updates_count": {
                          "type": "integer",
                          "description": "Present when include_details is true"
                        },
                        "latest_progress_update": {
                          "$ref": "#/components/schemas/EPMSProgressUpdate",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_complete": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_delete": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "requires_manager_approval": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "is_fully_approved": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "in_review_stage": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_cancel": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_put_on_hold": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_reactivate": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_send_to_employee": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_employee_review": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_manager_finalize": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_leadership_approve": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "manager_approved_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "leadership_approved_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "employee": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "john@example.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-01T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "EPMS Goals"
        ],
        "summary": "Delete goal",
        "description": "Deletes a goal if allowed based on status and permissions (can_delete?).\n\n**Required Scopes:** `write:epms_goals`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Goal ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Goal deleted successfully"
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/goals/{id}/complete": {
      "post": {
        "tags": [
          "EPMS Goals"
        ],
        "summary": "Complete goal",
        "description": "Marks a goal as complete. Requires 100% progress or explicit completion.\n\n**Required Scopes:** `write:epms_goals`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Goal ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Goal completed successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "goal": {
                      "type": "object",
                      "description": "Employee goal with progress tracking. Progress updates are returned as a sibling array in GET /goals/{id}, not nested in the goal object.",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 123
                        },
                        "title": {
                          "type": "string",
                          "example": "Increase sales by 20%"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "example": "Achieve 20% growth in Q1 sales"
                        },
                        "goal_type": {
                          "type": "string",
                          "enum": [
                            "performance",
                            "development",
                            "behavior",
                            "project",
                            "skill"
                          ],
                          "example": "performance"
                        },
                        "goal_type_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for goal type"
                        },
                        "goal_category": {
                          "type": "string",
                          "nullable": true,
                          "description": "The focus area category key for this goal",
                          "example": "professional_development"
                        },
                        "goal_category_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for the goal category",
                          "example": "Professional Development"
                        },
                        "priority": {
                          "type": "string",
                          "enum": [
                            "low",
                            "medium",
                            "high",
                            "critical"
                          ],
                          "example": "high"
                        },
                        "priority_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for priority"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "in_review",
                            "active",
                            "on_hold",
                            "completed",
                            "cancelled",
                            "overdue"
                          ],
                          "example": "active"
                        },
                        "display_status": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable display status"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-01"
                        },
                        "target_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-03-31"
                        },
                        "completed_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "progress_percentage": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 100,
                          "example": 45.5
                        },
                        "weight_percentage": {
                          "type": "number",
                          "nullable": true,
                          "example": 30.0
                        },
                        "workflow_stage": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "employee_review",
                            "manager_finalized",
                            "leadership_approved"
                          ],
                          "description": "Current workflow stage"
                        },
                        "workflow_stage_label": {
                          "type": "string",
                          "nullable": true
                        },
                        "is_smart_goal": {
                          "type": "boolean",
                          "example": true
                        },
                        "smart_score": {
                          "type": "number",
                          "nullable": true
                        },
                        "on_track": {
                          "type": "boolean",
                          "nullable": true
                        },
                        "days_until_due": {
                          "type": "integer",
                          "nullable": true
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        },
                        "progress_update_allowed": {
                          "type": "boolean",
                          "description": "Whether progress updates can be submitted for this goal"
                        },
                        "success_criteria": {
                          "type": "string",
                          "nullable": true,
                          "description": "Present when include_details is true (e.g. show endpoint)",
                          "example": "Reach $500K in sales"
                        },
                        "smart_criteria": {
                          "type": "object",
                          "nullable": true,
                          "description": "Present when include_details is true",
                          "properties": {
                            "is_specific": {
                              "type": "boolean"
                            },
                            "is_measurable": {
                              "type": "boolean"
                            },
                            "is_achievable": {
                              "type": "boolean"
                            },
                            "is_relevant": {
                              "type": "boolean"
                            },
                            "is_time_bound": {
                              "type": "boolean"
                            }
                          }
                        },
                        "progress_updates_count": {
                          "type": "integer",
                          "description": "Present when include_details is true"
                        },
                        "latest_progress_update": {
                          "$ref": "#/components/schemas/EPMSProgressUpdate",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_complete": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_delete": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "requires_manager_approval": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "is_fully_approved": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "in_review_stage": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_cancel": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_put_on_hold": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_reactivate": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_send_to_employee": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_employee_review": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_manager_finalize": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_leadership_approve": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "manager_approved_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "leadership_approved_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "employee": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "john@example.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-01T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        }
                      }
                    },
                    "message": {
                      "type": "string",
                      "example": "Goal has been completed successfully"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/goals/{id}/progress_updates": {
      "post": {
        "tags": [
          "EPMS Goals"
        ],
        "summary": "Add progress update",
        "description": "Adds a progress update to a goal. Can optionally update the goal's progress percentage.\n\n**Required Scopes:** `write:epms_goals`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Goal ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "progress_update"
                ],
                "properties": {
                  "progress_update": {
                    "type": "object",
                    "description": "Input schema for progress updates",
                    "properties": {
                      "progress_percentage": {
                        "type": "number",
                        "minimum": 0,
                        "maximum": 100,
                        "description": "Required for all types except 'comment'",
                        "example": 50.0
                      },
                      "update_notes": {
                        "type": "string",
                        "maxLength": 1000,
                        "example": "Halfway through the quarter, on track"
                      },
                      "update_type": {
                        "type": "string",
                        "enum": [
                          "regular",
                          "milestone",
                          "completion",
                          "revision",
                          "comment"
                        ],
                        "default": "regular",
                        "example": "regular"
                      },
                      "challenges_faced": {
                        "type": "string",
                        "nullable": true,
                        "description": "Obstacles or challenges encountered",
                        "example": "Dependency on external vendor delayed delivery"
                      },
                      "support_needed": {
                        "type": "string",
                        "nullable": true,
                        "description": "Support or resources needed",
                        "example": "Need additional budget approval for tooling"
                      },
                      "attachments": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        },
                        "nullable": true,
                        "description": "File references/attachment identifiers"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Progress update created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "progress_update": {
                      "type": "object",
                      "description": "Goal progress update record",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 789
                        },
                        "progress_percentage": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 100,
                          "example": 50.0
                        },
                        "update_notes": {
                          "type": "string",
                          "nullable": true,
                          "example": "Halfway through the quarter, on track"
                        },
                        "update_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-15"
                        },
                        "update_type": {
                          "type": "string",
                          "enum": [
                            "regular",
                            "milestone",
                            "completion",
                            "revision",
                            "comment"
                          ],
                          "example": "regular"
                        },
                        "challenges_faced": {
                          "type": "string",
                          "nullable": true
                        },
                        "support_needed": {
                          "type": "string",
                          "nullable": true
                        },
                        "attachments": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "nullable": true
                        },
                        "updated_by": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            }
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T10:30:00Z"
                        }
                      }
                    },
                    "goal": {
                      "type": "object",
                      "description": "Employee goal with progress tracking. Progress updates are returned as a sibling array in GET /goals/{id}, not nested in the goal object.",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 123
                        },
                        "title": {
                          "type": "string",
                          "example": "Increase sales by 20%"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "example": "Achieve 20% growth in Q1 sales"
                        },
                        "goal_type": {
                          "type": "string",
                          "enum": [
                            "performance",
                            "development",
                            "behavior",
                            "project",
                            "skill"
                          ],
                          "example": "performance"
                        },
                        "goal_type_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for goal type"
                        },
                        "goal_category": {
                          "type": "string",
                          "nullable": true,
                          "description": "The focus area category key for this goal",
                          "example": "professional_development"
                        },
                        "goal_category_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for the goal category",
                          "example": "Professional Development"
                        },
                        "priority": {
                          "type": "string",
                          "enum": [
                            "low",
                            "medium",
                            "high",
                            "critical"
                          ],
                          "example": "high"
                        },
                        "priority_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for priority"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "in_review",
                            "active",
                            "on_hold",
                            "completed",
                            "cancelled",
                            "overdue"
                          ],
                          "example": "active"
                        },
                        "display_status": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable display status"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-01"
                        },
                        "target_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-03-31"
                        },
                        "completed_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "progress_percentage": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 100,
                          "example": 45.5
                        },
                        "weight_percentage": {
                          "type": "number",
                          "nullable": true,
                          "example": 30.0
                        },
                        "workflow_stage": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "employee_review",
                            "manager_finalized",
                            "leadership_approved"
                          ],
                          "description": "Current workflow stage"
                        },
                        "workflow_stage_label": {
                          "type": "string",
                          "nullable": true
                        },
                        "is_smart_goal": {
                          "type": "boolean",
                          "example": true
                        },
                        "smart_score": {
                          "type": "number",
                          "nullable": true
                        },
                        "on_track": {
                          "type": "boolean",
                          "nullable": true
                        },
                        "days_until_due": {
                          "type": "integer",
                          "nullable": true
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        },
                        "progress_update_allowed": {
                          "type": "boolean",
                          "description": "Whether progress updates can be submitted for this goal"
                        },
                        "success_criteria": {
                          "type": "string",
                          "nullable": true,
                          "description": "Present when include_details is true (e.g. show endpoint)",
                          "example": "Reach $500K in sales"
                        },
                        "smart_criteria": {
                          "type": "object",
                          "nullable": true,
                          "description": "Present when include_details is true",
                          "properties": {
                            "is_specific": {
                              "type": "boolean"
                            },
                            "is_measurable": {
                              "type": "boolean"
                            },
                            "is_achievable": {
                              "type": "boolean"
                            },
                            "is_relevant": {
                              "type": "boolean"
                            },
                            "is_time_bound": {
                              "type": "boolean"
                            }
                          }
                        },
                        "progress_updates_count": {
                          "type": "integer",
                          "description": "Present when include_details is true"
                        },
                        "latest_progress_update": {
                          "$ref": "#/components/schemas/EPMSProgressUpdate",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_complete": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_delete": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "requires_manager_approval": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "is_fully_approved": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "in_review_stage": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_cancel": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_put_on_hold": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_reactivate": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_send_to_employee": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_employee_review": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_manager_finalize": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_leadership_approve": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "manager_approved_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "leadership_approved_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "employee": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "john@example.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-01T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/goals/{id}/submit_employee_changes": {
      "post": {
        "tags": [
          "EPMS Goals"
        ],
        "summary": "Submit employee changes",
        "description": "Employee submits reviewed goal to manager (workflow: employee_review → manager_finalized).\nOnly the goal owner can call this endpoint.\n**Required Scopes:** `write:epms_goals`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Goal ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "comments": {
                    "type": "string",
                    "description": "Optional comments when submitting"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Goal submitted successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "goal": {
                      "type": "object",
                      "description": "Employee goal with progress tracking. Progress updates are returned as a sibling array in GET /goals/{id}, not nested in the goal object.",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 123
                        },
                        "title": {
                          "type": "string",
                          "example": "Increase sales by 20%"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "example": "Achieve 20% growth in Q1 sales"
                        },
                        "goal_type": {
                          "type": "string",
                          "enum": [
                            "performance",
                            "development",
                            "behavior",
                            "project",
                            "skill"
                          ],
                          "example": "performance"
                        },
                        "goal_type_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for goal type"
                        },
                        "goal_category": {
                          "type": "string",
                          "nullable": true,
                          "description": "The focus area category key for this goal",
                          "example": "professional_development"
                        },
                        "goal_category_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for the goal category",
                          "example": "Professional Development"
                        },
                        "priority": {
                          "type": "string",
                          "enum": [
                            "low",
                            "medium",
                            "high",
                            "critical"
                          ],
                          "example": "high"
                        },
                        "priority_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for priority"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "in_review",
                            "active",
                            "on_hold",
                            "completed",
                            "cancelled",
                            "overdue"
                          ],
                          "example": "active"
                        },
                        "display_status": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable display status"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-01"
                        },
                        "target_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-03-31"
                        },
                        "completed_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "progress_percentage": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 100,
                          "example": 45.5
                        },
                        "weight_percentage": {
                          "type": "number",
                          "nullable": true,
                          "example": 30.0
                        },
                        "workflow_stage": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "employee_review",
                            "manager_finalized",
                            "leadership_approved"
                          ],
                          "description": "Current workflow stage"
                        },
                        "workflow_stage_label": {
                          "type": "string",
                          "nullable": true
                        },
                        "is_smart_goal": {
                          "type": "boolean",
                          "example": true
                        },
                        "smart_score": {
                          "type": "number",
                          "nullable": true
                        },
                        "on_track": {
                          "type": "boolean",
                          "nullable": true
                        },
                        "days_until_due": {
                          "type": "integer",
                          "nullable": true
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        },
                        "progress_update_allowed": {
                          "type": "boolean",
                          "description": "Whether progress updates can be submitted for this goal"
                        },
                        "success_criteria": {
                          "type": "string",
                          "nullable": true,
                          "description": "Present when include_details is true (e.g. show endpoint)",
                          "example": "Reach $500K in sales"
                        },
                        "smart_criteria": {
                          "type": "object",
                          "nullable": true,
                          "description": "Present when include_details is true",
                          "properties": {
                            "is_specific": {
                              "type": "boolean"
                            },
                            "is_measurable": {
                              "type": "boolean"
                            },
                            "is_achievable": {
                              "type": "boolean"
                            },
                            "is_relevant": {
                              "type": "boolean"
                            },
                            "is_time_bound": {
                              "type": "boolean"
                            }
                          }
                        },
                        "progress_updates_count": {
                          "type": "integer",
                          "description": "Present when include_details is true"
                        },
                        "latest_progress_update": {
                          "$ref": "#/components/schemas/EPMSProgressUpdate",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_complete": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_delete": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "requires_manager_approval": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "is_fully_approved": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "in_review_stage": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_cancel": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_put_on_hold": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_reactivate": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_send_to_employee": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_employee_review": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_manager_finalize": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_leadership_approve": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "manager_approved_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "leadership_approved_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "employee": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "john@example.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-01T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/goals/{id}/submit_for_leadership_approval": {
      "post": {
        "tags": [
          "EPMS Goals"
        ],
        "summary": "Submit for leadership approval",
        "description": "Manager finalizes goal and optionally submits for leadership approval (requires can_finalize_goal?).\n**Required Scopes:** `write:epms_goals`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Goal ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "comments": {
                    "type": "string",
                    "description": "Optional comments"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Goal finalized / submitted for leadership approval",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "goal": {
                      "type": "object",
                      "description": "Employee goal with progress tracking. Progress updates are returned as a sibling array in GET /goals/{id}, not nested in the goal object.",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 123
                        },
                        "title": {
                          "type": "string",
                          "example": "Increase sales by 20%"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "example": "Achieve 20% growth in Q1 sales"
                        },
                        "goal_type": {
                          "type": "string",
                          "enum": [
                            "performance",
                            "development",
                            "behavior",
                            "project",
                            "skill"
                          ],
                          "example": "performance"
                        },
                        "goal_type_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for goal type"
                        },
                        "goal_category": {
                          "type": "string",
                          "nullable": true,
                          "description": "The focus area category key for this goal",
                          "example": "professional_development"
                        },
                        "goal_category_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for the goal category",
                          "example": "Professional Development"
                        },
                        "priority": {
                          "type": "string",
                          "enum": [
                            "low",
                            "medium",
                            "high",
                            "critical"
                          ],
                          "example": "high"
                        },
                        "priority_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for priority"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "in_review",
                            "active",
                            "on_hold",
                            "completed",
                            "cancelled",
                            "overdue"
                          ],
                          "example": "active"
                        },
                        "display_status": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable display status"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-01"
                        },
                        "target_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-03-31"
                        },
                        "completed_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "progress_percentage": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 100,
                          "example": 45.5
                        },
                        "weight_percentage": {
                          "type": "number",
                          "nullable": true,
                          "example": 30.0
                        },
                        "workflow_stage": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "employee_review",
                            "manager_finalized",
                            "leadership_approved"
                          ],
                          "description": "Current workflow stage"
                        },
                        "workflow_stage_label": {
                          "type": "string",
                          "nullable": true
                        },
                        "is_smart_goal": {
                          "type": "boolean",
                          "example": true
                        },
                        "smart_score": {
                          "type": "number",
                          "nullable": true
                        },
                        "on_track": {
                          "type": "boolean",
                          "nullable": true
                        },
                        "days_until_due": {
                          "type": "integer",
                          "nullable": true
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        },
                        "progress_update_allowed": {
                          "type": "boolean",
                          "description": "Whether progress updates can be submitted for this goal"
                        },
                        "success_criteria": {
                          "type": "string",
                          "nullable": true,
                          "description": "Present when include_details is true (e.g. show endpoint)",
                          "example": "Reach $500K in sales"
                        },
                        "smart_criteria": {
                          "type": "object",
                          "nullable": true,
                          "description": "Present when include_details is true",
                          "properties": {
                            "is_specific": {
                              "type": "boolean"
                            },
                            "is_measurable": {
                              "type": "boolean"
                            },
                            "is_achievable": {
                              "type": "boolean"
                            },
                            "is_relevant": {
                              "type": "boolean"
                            },
                            "is_time_bound": {
                              "type": "boolean"
                            }
                          }
                        },
                        "progress_updates_count": {
                          "type": "integer",
                          "description": "Present when include_details is true"
                        },
                        "latest_progress_update": {
                          "$ref": "#/components/schemas/EPMSProgressUpdate",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_complete": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_delete": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "requires_manager_approval": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "is_fully_approved": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "in_review_stage": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_cancel": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_put_on_hold": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_reactivate": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_send_to_employee": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_employee_review": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_manager_finalize": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_leadership_approve": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "manager_approved_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "leadership_approved_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "employee": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "john@example.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-01T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        }
                      }
                    },
                    "message": {
                      "type": "string",
                      "example": "Goal approved successfully."
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/goals/{id}/leadership_approve": {
      "post": {
        "tags": [
          "EPMS Goals"
        ],
        "summary": "Leadership approve",
        "description": "Administrator performs leadership approval (manager_finalized → leadership_approved).\n**Required Scopes:** `write:epms_goals`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Goal ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "comments": {
                    "type": "string",
                    "description": "Optional comments"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Goal approved by leadership",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "goal": {
                      "type": "object",
                      "description": "Employee goal with progress tracking. Progress updates are returned as a sibling array in GET /goals/{id}, not nested in the goal object.",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 123
                        },
                        "title": {
                          "type": "string",
                          "example": "Increase sales by 20%"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "example": "Achieve 20% growth in Q1 sales"
                        },
                        "goal_type": {
                          "type": "string",
                          "enum": [
                            "performance",
                            "development",
                            "behavior",
                            "project",
                            "skill"
                          ],
                          "example": "performance"
                        },
                        "goal_type_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for goal type"
                        },
                        "goal_category": {
                          "type": "string",
                          "nullable": true,
                          "description": "The focus area category key for this goal",
                          "example": "professional_development"
                        },
                        "goal_category_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for the goal category",
                          "example": "Professional Development"
                        },
                        "priority": {
                          "type": "string",
                          "enum": [
                            "low",
                            "medium",
                            "high",
                            "critical"
                          ],
                          "example": "high"
                        },
                        "priority_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for priority"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "in_review",
                            "active",
                            "on_hold",
                            "completed",
                            "cancelled",
                            "overdue"
                          ],
                          "example": "active"
                        },
                        "display_status": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable display status"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-01"
                        },
                        "target_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-03-31"
                        },
                        "completed_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "progress_percentage": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 100,
                          "example": 45.5
                        },
                        "weight_percentage": {
                          "type": "number",
                          "nullable": true,
                          "example": 30.0
                        },
                        "workflow_stage": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "employee_review",
                            "manager_finalized",
                            "leadership_approved"
                          ],
                          "description": "Current workflow stage"
                        },
                        "workflow_stage_label": {
                          "type": "string",
                          "nullable": true
                        },
                        "is_smart_goal": {
                          "type": "boolean",
                          "example": true
                        },
                        "smart_score": {
                          "type": "number",
                          "nullable": true
                        },
                        "on_track": {
                          "type": "boolean",
                          "nullable": true
                        },
                        "days_until_due": {
                          "type": "integer",
                          "nullable": true
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        },
                        "progress_update_allowed": {
                          "type": "boolean",
                          "description": "Whether progress updates can be submitted for this goal"
                        },
                        "success_criteria": {
                          "type": "string",
                          "nullable": true,
                          "description": "Present when include_details is true (e.g. show endpoint)",
                          "example": "Reach $500K in sales"
                        },
                        "smart_criteria": {
                          "type": "object",
                          "nullable": true,
                          "description": "Present when include_details is true",
                          "properties": {
                            "is_specific": {
                              "type": "boolean"
                            },
                            "is_measurable": {
                              "type": "boolean"
                            },
                            "is_achievable": {
                              "type": "boolean"
                            },
                            "is_relevant": {
                              "type": "boolean"
                            },
                            "is_time_bound": {
                              "type": "boolean"
                            }
                          }
                        },
                        "progress_updates_count": {
                          "type": "integer",
                          "description": "Present when include_details is true"
                        },
                        "latest_progress_update": {
                          "$ref": "#/components/schemas/EPMSProgressUpdate",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_complete": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_delete": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "requires_manager_approval": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "is_fully_approved": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "in_review_stage": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_cancel": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_put_on_hold": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_reactivate": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_send_to_employee": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_employee_review": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_manager_finalize": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_leadership_approve": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "manager_approved_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "leadership_approved_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "employee": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "john@example.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-01T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        }
                      }
                    },
                    "message": {
                      "type": "string",
                      "example": "Goal approved by leadership."
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/goals/{id}/leadership_reject": {
      "post": {
        "tags": [
          "EPMS Goals"
        ],
        "summary": "Leadership reject",
        "description": "Administrator rejects goal and sends back for revision (requires comments).\n**Required Scopes:** `write:epms_goals`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Goal ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "comments"
                ],
                "properties": {
                  "comments": {
                    "type": "string",
                    "description": "Required rejection comments"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Goal rejected and sent back for revision",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "goal": {
                      "type": "object",
                      "description": "Employee goal with progress tracking. Progress updates are returned as a sibling array in GET /goals/{id}, not nested in the goal object.",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 123
                        },
                        "title": {
                          "type": "string",
                          "example": "Increase sales by 20%"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "example": "Achieve 20% growth in Q1 sales"
                        },
                        "goal_type": {
                          "type": "string",
                          "enum": [
                            "performance",
                            "development",
                            "behavior",
                            "project",
                            "skill"
                          ],
                          "example": "performance"
                        },
                        "goal_type_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for goal type"
                        },
                        "goal_category": {
                          "type": "string",
                          "nullable": true,
                          "description": "The focus area category key for this goal",
                          "example": "professional_development"
                        },
                        "goal_category_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for the goal category",
                          "example": "Professional Development"
                        },
                        "priority": {
                          "type": "string",
                          "enum": [
                            "low",
                            "medium",
                            "high",
                            "critical"
                          ],
                          "example": "high"
                        },
                        "priority_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for priority"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "in_review",
                            "active",
                            "on_hold",
                            "completed",
                            "cancelled",
                            "overdue"
                          ],
                          "example": "active"
                        },
                        "display_status": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable display status"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-01"
                        },
                        "target_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-03-31"
                        },
                        "completed_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "progress_percentage": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 100,
                          "example": 45.5
                        },
                        "weight_percentage": {
                          "type": "number",
                          "nullable": true,
                          "example": 30.0
                        },
                        "workflow_stage": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "employee_review",
                            "manager_finalized",
                            "leadership_approved"
                          ],
                          "description": "Current workflow stage"
                        },
                        "workflow_stage_label": {
                          "type": "string",
                          "nullable": true
                        },
                        "is_smart_goal": {
                          "type": "boolean",
                          "example": true
                        },
                        "smart_score": {
                          "type": "number",
                          "nullable": true
                        },
                        "on_track": {
                          "type": "boolean",
                          "nullable": true
                        },
                        "days_until_due": {
                          "type": "integer",
                          "nullable": true
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        },
                        "progress_update_allowed": {
                          "type": "boolean",
                          "description": "Whether progress updates can be submitted for this goal"
                        },
                        "success_criteria": {
                          "type": "string",
                          "nullable": true,
                          "description": "Present when include_details is true (e.g. show endpoint)",
                          "example": "Reach $500K in sales"
                        },
                        "smart_criteria": {
                          "type": "object",
                          "nullable": true,
                          "description": "Present when include_details is true",
                          "properties": {
                            "is_specific": {
                              "type": "boolean"
                            },
                            "is_measurable": {
                              "type": "boolean"
                            },
                            "is_achievable": {
                              "type": "boolean"
                            },
                            "is_relevant": {
                              "type": "boolean"
                            },
                            "is_time_bound": {
                              "type": "boolean"
                            }
                          }
                        },
                        "progress_updates_count": {
                          "type": "integer",
                          "description": "Present when include_details is true"
                        },
                        "latest_progress_update": {
                          "$ref": "#/components/schemas/EPMSProgressUpdate",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_complete": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_delete": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "requires_manager_approval": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "is_fully_approved": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "in_review_stage": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_cancel": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_put_on_hold": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_reactivate": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_send_to_employee": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_employee_review": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_manager_finalize": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_leadership_approve": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "manager_approved_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "leadership_approved_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "employee": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "john@example.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-01T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        }
                      }
                    },
                    "message": {
                      "type": "string",
                      "example": "Goal rejected and sent back for revision."
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/goals/{id}/manager_reject": {
      "post": {
        "tags": [
          "EPMS Goals"
        ],
        "summary": "Manager reject",
        "description": "Manager rejects goal and sends back to employee (comment or rejection_comment required).\n**Required Scopes:** `write:epms_goals`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Goal ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "comment": {
                    "type": "string",
                    "description": "Comment when requesting changes (alternative to rejection_comment)"
                  },
                  "rejection_comment": {
                    "type": "string",
                    "description": "Rejection comment (alternative to comment)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Goal sent back to employee with feedback",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "goal": {
                      "type": "object",
                      "description": "Employee goal with progress tracking. Progress updates are returned as a sibling array in GET /goals/{id}, not nested in the goal object.",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 123
                        },
                        "title": {
                          "type": "string",
                          "example": "Increase sales by 20%"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "example": "Achieve 20% growth in Q1 sales"
                        },
                        "goal_type": {
                          "type": "string",
                          "enum": [
                            "performance",
                            "development",
                            "behavior",
                            "project",
                            "skill"
                          ],
                          "example": "performance"
                        },
                        "goal_type_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for goal type"
                        },
                        "goal_category": {
                          "type": "string",
                          "nullable": true,
                          "description": "The focus area category key for this goal",
                          "example": "professional_development"
                        },
                        "goal_category_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for the goal category",
                          "example": "Professional Development"
                        },
                        "priority": {
                          "type": "string",
                          "enum": [
                            "low",
                            "medium",
                            "high",
                            "critical"
                          ],
                          "example": "high"
                        },
                        "priority_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for priority"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "in_review",
                            "active",
                            "on_hold",
                            "completed",
                            "cancelled",
                            "overdue"
                          ],
                          "example": "active"
                        },
                        "display_status": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable display status"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-01"
                        },
                        "target_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-03-31"
                        },
                        "completed_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "progress_percentage": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 100,
                          "example": 45.5
                        },
                        "weight_percentage": {
                          "type": "number",
                          "nullable": true,
                          "example": 30.0
                        },
                        "workflow_stage": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "employee_review",
                            "manager_finalized",
                            "leadership_approved"
                          ],
                          "description": "Current workflow stage"
                        },
                        "workflow_stage_label": {
                          "type": "string",
                          "nullable": true
                        },
                        "is_smart_goal": {
                          "type": "boolean",
                          "example": true
                        },
                        "smart_score": {
                          "type": "number",
                          "nullable": true
                        },
                        "on_track": {
                          "type": "boolean",
                          "nullable": true
                        },
                        "days_until_due": {
                          "type": "integer",
                          "nullable": true
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        },
                        "progress_update_allowed": {
                          "type": "boolean",
                          "description": "Whether progress updates can be submitted for this goal"
                        },
                        "success_criteria": {
                          "type": "string",
                          "nullable": true,
                          "description": "Present when include_details is true (e.g. show endpoint)",
                          "example": "Reach $500K in sales"
                        },
                        "smart_criteria": {
                          "type": "object",
                          "nullable": true,
                          "description": "Present when include_details is true",
                          "properties": {
                            "is_specific": {
                              "type": "boolean"
                            },
                            "is_measurable": {
                              "type": "boolean"
                            },
                            "is_achievable": {
                              "type": "boolean"
                            },
                            "is_relevant": {
                              "type": "boolean"
                            },
                            "is_time_bound": {
                              "type": "boolean"
                            }
                          }
                        },
                        "progress_updates_count": {
                          "type": "integer",
                          "description": "Present when include_details is true"
                        },
                        "latest_progress_update": {
                          "$ref": "#/components/schemas/EPMSProgressUpdate",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_complete": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_delete": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "requires_manager_approval": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "is_fully_approved": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "in_review_stage": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_cancel": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_put_on_hold": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_reactivate": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_send_to_employee": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_employee_review": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_manager_finalize": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_leadership_approve": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "manager_approved_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "leadership_approved_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "employee": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "john@example.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-01T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        }
                      }
                    },
                    "message": {
                      "type": "string",
                      "example": "Goal sent back to employee with your feedback."
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/goals/{id}/cancel_change_request": {
      "post": {
        "tags": [
          "EPMS Goals"
        ],
        "summary": "Cancel change request",
        "description": "Manager cancels their rejection (employee_review → manager_finalized).\n**Required Scopes:** `write:epms_goals`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Goal ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Change request cancelled",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "goal": {
                      "type": "object",
                      "description": "Employee goal with progress tracking. Progress updates are returned as a sibling array in GET /goals/{id}, not nested in the goal object.",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 123
                        },
                        "title": {
                          "type": "string",
                          "example": "Increase sales by 20%"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "example": "Achieve 20% growth in Q1 sales"
                        },
                        "goal_type": {
                          "type": "string",
                          "enum": [
                            "performance",
                            "development",
                            "behavior",
                            "project",
                            "skill"
                          ],
                          "example": "performance"
                        },
                        "goal_type_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for goal type"
                        },
                        "goal_category": {
                          "type": "string",
                          "nullable": true,
                          "description": "The focus area category key for this goal",
                          "example": "professional_development"
                        },
                        "goal_category_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for the goal category",
                          "example": "Professional Development"
                        },
                        "priority": {
                          "type": "string",
                          "enum": [
                            "low",
                            "medium",
                            "high",
                            "critical"
                          ],
                          "example": "high"
                        },
                        "priority_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for priority"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "in_review",
                            "active",
                            "on_hold",
                            "completed",
                            "cancelled",
                            "overdue"
                          ],
                          "example": "active"
                        },
                        "display_status": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable display status"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-01"
                        },
                        "target_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-03-31"
                        },
                        "completed_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "progress_percentage": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 100,
                          "example": 45.5
                        },
                        "weight_percentage": {
                          "type": "number",
                          "nullable": true,
                          "example": 30.0
                        },
                        "workflow_stage": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "employee_review",
                            "manager_finalized",
                            "leadership_approved"
                          ],
                          "description": "Current workflow stage"
                        },
                        "workflow_stage_label": {
                          "type": "string",
                          "nullable": true
                        },
                        "is_smart_goal": {
                          "type": "boolean",
                          "example": true
                        },
                        "smart_score": {
                          "type": "number",
                          "nullable": true
                        },
                        "on_track": {
                          "type": "boolean",
                          "nullable": true
                        },
                        "days_until_due": {
                          "type": "integer",
                          "nullable": true
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        },
                        "progress_update_allowed": {
                          "type": "boolean",
                          "description": "Whether progress updates can be submitted for this goal"
                        },
                        "success_criteria": {
                          "type": "string",
                          "nullable": true,
                          "description": "Present when include_details is true (e.g. show endpoint)",
                          "example": "Reach $500K in sales"
                        },
                        "smart_criteria": {
                          "type": "object",
                          "nullable": true,
                          "description": "Present when include_details is true",
                          "properties": {
                            "is_specific": {
                              "type": "boolean"
                            },
                            "is_measurable": {
                              "type": "boolean"
                            },
                            "is_achievable": {
                              "type": "boolean"
                            },
                            "is_relevant": {
                              "type": "boolean"
                            },
                            "is_time_bound": {
                              "type": "boolean"
                            }
                          }
                        },
                        "progress_updates_count": {
                          "type": "integer",
                          "description": "Present when include_details is true"
                        },
                        "latest_progress_update": {
                          "$ref": "#/components/schemas/EPMSProgressUpdate",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_complete": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_delete": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "requires_manager_approval": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "is_fully_approved": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "in_review_stage": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_cancel": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_put_on_hold": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_reactivate": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_send_to_employee": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_employee_review": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_manager_finalize": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_leadership_approve": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "manager_approved_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "leadership_approved_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "employee": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "john@example.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-01T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        }
                      }
                    },
                    "message": {
                      "type": "string",
                      "example": "Change request cancelled. Goal is ready for manager review."
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/goals/{id}/add_comment": {
      "post": {
        "tags": [
          "EPMS Goals"
        ],
        "summary": "Add comment",
        "description": "Adds a comment (GoalProgressUpdate with update_type 'comment') to a goal.\nRequires can_manage_employee_data? for the goal's employee.\n**Required Scopes:** `write:epms_goals`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Goal ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "comment"
                ],
                "properties": {
                  "comment": {
                    "type": "string",
                    "description": "Comment text (required)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Comment created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "comment": {
                      "type": "object",
                      "description": "Goal progress update record",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 789
                        },
                        "progress_percentage": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 100,
                          "example": 50.0
                        },
                        "update_notes": {
                          "type": "string",
                          "nullable": true,
                          "example": "Halfway through the quarter, on track"
                        },
                        "update_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-15"
                        },
                        "update_type": {
                          "type": "string",
                          "enum": [
                            "regular",
                            "milestone",
                            "completion",
                            "revision",
                            "comment"
                          ],
                          "example": "regular"
                        },
                        "challenges_faced": {
                          "type": "string",
                          "nullable": true
                        },
                        "support_needed": {
                          "type": "string",
                          "nullable": true
                        },
                        "attachments": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "nullable": true
                        },
                        "updated_by": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            }
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T10:30:00Z"
                        }
                      }
                    },
                    "goal": {
                      "type": "object",
                      "description": "Employee goal with progress tracking. Progress updates are returned as a sibling array in GET /goals/{id}, not nested in the goal object.",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 123
                        },
                        "title": {
                          "type": "string",
                          "example": "Increase sales by 20%"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "example": "Achieve 20% growth in Q1 sales"
                        },
                        "goal_type": {
                          "type": "string",
                          "enum": [
                            "performance",
                            "development",
                            "behavior",
                            "project",
                            "skill"
                          ],
                          "example": "performance"
                        },
                        "goal_type_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for goal type"
                        },
                        "goal_category": {
                          "type": "string",
                          "nullable": true,
                          "description": "The focus area category key for this goal",
                          "example": "professional_development"
                        },
                        "goal_category_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for the goal category",
                          "example": "Professional Development"
                        },
                        "priority": {
                          "type": "string",
                          "enum": [
                            "low",
                            "medium",
                            "high",
                            "critical"
                          ],
                          "example": "high"
                        },
                        "priority_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for priority"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "in_review",
                            "active",
                            "on_hold",
                            "completed",
                            "cancelled",
                            "overdue"
                          ],
                          "example": "active"
                        },
                        "display_status": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable display status"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-01"
                        },
                        "target_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-03-31"
                        },
                        "completed_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "progress_percentage": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 100,
                          "example": 45.5
                        },
                        "weight_percentage": {
                          "type": "number",
                          "nullable": true,
                          "example": 30.0
                        },
                        "workflow_stage": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "employee_review",
                            "manager_finalized",
                            "leadership_approved"
                          ],
                          "description": "Current workflow stage"
                        },
                        "workflow_stage_label": {
                          "type": "string",
                          "nullable": true
                        },
                        "is_smart_goal": {
                          "type": "boolean",
                          "example": true
                        },
                        "smart_score": {
                          "type": "number",
                          "nullable": true
                        },
                        "on_track": {
                          "type": "boolean",
                          "nullable": true
                        },
                        "days_until_due": {
                          "type": "integer",
                          "nullable": true
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        },
                        "progress_update_allowed": {
                          "type": "boolean",
                          "description": "Whether progress updates can be submitted for this goal"
                        },
                        "success_criteria": {
                          "type": "string",
                          "nullable": true,
                          "description": "Present when include_details is true (e.g. show endpoint)",
                          "example": "Reach $500K in sales"
                        },
                        "smart_criteria": {
                          "type": "object",
                          "nullable": true,
                          "description": "Present when include_details is true",
                          "properties": {
                            "is_specific": {
                              "type": "boolean"
                            },
                            "is_measurable": {
                              "type": "boolean"
                            },
                            "is_achievable": {
                              "type": "boolean"
                            },
                            "is_relevant": {
                              "type": "boolean"
                            },
                            "is_time_bound": {
                              "type": "boolean"
                            }
                          }
                        },
                        "progress_updates_count": {
                          "type": "integer",
                          "description": "Present when include_details is true"
                        },
                        "latest_progress_update": {
                          "$ref": "#/components/schemas/EPMSProgressUpdate",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_complete": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_delete": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "requires_manager_approval": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "is_fully_approved": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "in_review_stage": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_cancel": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_put_on_hold": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_reactivate": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_send_to_employee": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_employee_review": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_manager_finalize": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_leadership_approve": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "manager_approved_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "leadership_approved_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "employee": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "john@example.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-01T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/goals/stats": {
      "get": {
        "tags": [
          "EPMS Goals"
        ],
        "summary": "Get goal statistics",
        "description": "Retrieves goal statistics for dashboard display. Can be filtered by employee ID.\n\n**Required Scopes:** `read:epms_goals`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "employee_id",
            "in": "query",
            "description": "Optional filter by employee ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Goal statistics retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "stats": {
                      "type": "object",
                      "description": "Goal statistics for dashboard",
                      "properties": {
                        "total_goals": {
                          "type": "integer",
                          "example": 20
                        },
                        "active_goals": {
                          "type": "integer",
                          "example": 12
                        },
                        "completed_goals": {
                          "type": "integer",
                          "example": 6
                        },
                        "overdue_goals": {
                          "type": "integer",
                          "example": 2
                        },
                        "on_hold_goals": {
                          "type": "integer",
                          "example": 0
                        },
                        "cancelled_goals": {
                          "type": "integer",
                          "example": 0
                        },
                        "due_soon_count": {
                          "type": "integer",
                          "description": "Goals due within the next 7 days",
                          "example": 3
                        },
                        "average_progress": {
                          "type": "number",
                          "example": 45.5
                        },
                        "completion_rate": {
                          "type": "number",
                          "description": "Percentage of completed goals (0-100)",
                          "example": 30.0
                        },
                        "by_status": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "integer"
                          },
                          "description": "Counts per status (draft, active, on_hold, completed, cancelled, in_review, overdue)"
                        },
                        "by_priority": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "integer"
                          },
                          "description": "Counts per priority (low, medium, high, critical)"
                        },
                        "by_type": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "integer"
                          },
                          "description": "Counts per goal type (performance, development, behavior, project, skill)"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/goals/categories": {
      "get": {
        "tags": [
          "EPMS Goals"
        ],
        "summary": "List goal categories",
        "description": "Returns valid goal categories filtered by employee role. Categories are role-based\n(leader vs individual contributor). Without `employee_id`, returns categories for\nthe authenticated user's role. With `employee_id`, returns categories for that\nemployee's role (requires read access to that employee).\n\n**Required Scopes:** `read:epms_goals`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "employee_id",
            "in": "query",
            "description": "Get categories for this employee's role type. Defaults to the authenticated user if omitted.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Goal categories retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Response containing valid goal categories for a user's role",
                  "properties": {
                    "categories": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EPMSGoalCategoryOption"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/goals/types": {
      "get": {
        "tags": [
          "EPMS Goals"
        ],
        "summary": "List goal types",
        "description": "Returns valid goal types configured for the business, each with a value and label.\n\n**Required Scopes:** `read:epms_goals`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Goal types retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "goal_types": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "value": {
                            "type": "string",
                            "example": "performance"
                          },
                          "label": {
                            "type": "string",
                            "example": "Performance Goal"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/goals/{id}/analyze_smart": {
      "post": {
        "tags": [
          "EPMS Goals"
        ],
        "summary": "AI SMART analysis for a saved goal",
        "description": "Runs AI-powered SMART criteria analysis on an existing goal. Scores each criterion\n(Specific, Measurable, Achievable, Relevant, Time-bound) from 1-10, provides feedback\nand suggestions, and returns an improved title and description. The analysis results\nare persisted on the goal record.\n\n**Required Scopes:** `write:epms_goals`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "SMART analysis completed successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "overall_score": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 100,
                      "example": 75
                    },
                    "criteria": {
                      "type": "object",
                      "properties": {
                        "specific": {
                          "$ref": "#/components/schemas/EPMSSmartCriterionResult"
                        },
                        "measurable": {
                          "$ref": "#/components/schemas/EPMSSmartCriterionResult"
                        },
                        "achievable": {
                          "$ref": "#/components/schemas/EPMSSmartCriterionResult"
                        },
                        "relevant": {
                          "$ref": "#/components/schemas/EPMSSmartCriterionResult"
                        },
                        "time_bound": {
                          "$ref": "#/components/schemas/EPMSSmartCriterionResult"
                        }
                      }
                    },
                    "improved_title": {
                      "type": "string",
                      "nullable": true
                    },
                    "improved_description": {
                      "type": "string",
                      "nullable": true
                    },
                    "summary": {
                      "type": "string",
                      "nullable": true
                    },
                    "goal": {
                      "$ref": "#/components/schemas/EPMSGoal"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          }
        }
      }
    },
    "/epms/goals/analyze_smart_text": {
      "post": {
        "tags": [
          "EPMS Goals"
        ],
        "summary": "AI SMART analysis for goal text (before saving)",
        "description": "Runs AI-powered SMART criteria analysis on goal text before the goal is saved.\nUseful for providing real-time feedback during goal creation. Scores each criterion\nfrom 1-10 and suggests improvements.\n\n**Required Scopes:** `write:epms_goals`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "title",
                  "description"
                ],
                "properties": {
                  "title": {
                    "type": "string",
                    "description": "The goal title to analyze"
                  },
                  "description": {
                    "type": "string",
                    "description": "The goal description to analyze"
                  },
                  "target_date": {
                    "type": "string",
                    "format": "date",
                    "description": "Optional target date for time-bound analysis"
                  },
                  "success_criteria": {
                    "type": "string",
                    "description": "Optional success criteria"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "SMART text analysis completed successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "overall_score": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 100
                    },
                    "criteria": {
                      "type": "object",
                      "properties": {
                        "specific": {
                          "$ref": "#/components/schemas/EPMSSmartCriterionResult"
                        },
                        "measurable": {
                          "$ref": "#/components/schemas/EPMSSmartCriterionResult"
                        },
                        "achievable": {
                          "$ref": "#/components/schemas/EPMSSmartCriterionResult"
                        },
                        "relevant": {
                          "$ref": "#/components/schemas/EPMSSmartCriterionResult"
                        },
                        "time_bound": {
                          "$ref": "#/components/schemas/EPMSSmartCriterionResult"
                        }
                      }
                    },
                    "improved_title": {
                      "type": "string",
                      "nullable": true
                    },
                    "improved_description": {
                      "type": "string",
                      "nullable": true
                    },
                    "summary": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          }
        }
      }
    },
    "/epms/goal_templates": {
      "get": {
        "tags": [
          "EPMS Goal Templates"
        ],
        "summary": "List goal templates",
        "description": "Returns active goal templates for the business, with optional filtering by goal type.\nTemplates provide pre-configured goal structures that can be used to create new goals.\n\n**Required Scopes:** `read:epms_goals`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "description": "Number of items per page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "goal_type",
            "in": "query",
            "description": "Filter templates by goal type",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Goal templates retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EPMSGoalTemplate"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/goal_templates/{id}": {
      "get": {
        "tags": [
          "EPMS Goal Templates"
        ],
        "summary": "Get goal template details",
        "description": "Returns detailed information about a specific goal template, including success criteria,\nSMART criteria, measurement method, and usage statistics.\n\n**Required Scopes:** `read:epms_goals`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Goal template details retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "goal_template": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/EPMSGoalTemplate"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "success_criteria": {
                              "type": "string",
                              "nullable": true
                            },
                            "resources_needed": {
                              "type": "string",
                              "nullable": true
                            },
                            "measurement_method": {
                              "type": "string",
                              "nullable": true
                            },
                            "smart_criteria": {
                              "type": "object",
                              "properties": {
                                "is_specific": {
                                  "type": "boolean"
                                },
                                "is_measurable": {
                                  "type": "boolean"
                                },
                                "is_achievable": {
                                  "type": "boolean"
                                },
                                "is_relevant": {
                                  "type": "boolean"
                                },
                                "is_time_bound": {
                                  "type": "boolean"
                                }
                              }
                            },
                            "missing_smart_criteria": {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            },
                            "usage_statistics": {
                              "type": "object",
                              "properties": {
                                "total_goals": {
                                  "type": "integer"
                                },
                                "active_goals": {
                                  "type": "integer"
                                },
                                "completed_goals": {
                                  "type": "integer"
                                },
                                "completion_rate": {
                                  "type": "number"
                                },
                                "last_used": {
                                  "type": "string",
                                  "format": "date-time",
                                  "nullable": true
                                }
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/goals/creation_config": {
      "get": {
        "tags": [
          "EPMS Goals"
        ],
        "summary": "Goal creation configuration",
        "description": "Returns all dynamic properties needed to render the goal create form: goal templates,\ngoal types, priorities, goal categories, SMART goal checklist configuration, and\nwhether approval is required. Without `employee_id`, returns categories for\nthe authenticated user's role. With `employee_id`, returns role-specific categories\nfor that employee.\n\n**Required Scopes:** `read:epms_goals`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "employee_id",
            "in": "query",
            "description": "Get role-specific categories for this employee. Defaults to the authenticated user if omitted.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Goal creation configuration retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "All dynamic properties needed to render the goal create form",
                  "properties": {
                    "goal_templates": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EPMSGoalTemplateOption"
                      }
                    },
                    "goal_types": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EPMSGoalCategoryOption"
                      }
                    },
                    "priorities": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EPMSGoalCategoryOption"
                      }
                    },
                    "goal_categories": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EPMSGoalCategoryOption"
                      }
                    },
                    "goal_checklist": {
                      "$ref": "#/components/schemas/EPMSGoalChecklist"
                    },
                    "requires_approval": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/performance_reviews": {
      "get": {
        "tags": [
          "EPMS Performance Reviews"
        ],
        "summary": "List performance reviews",
        "description": "Retrieves a list of performance reviews with filtering options. Supports filtering by scope, status, type, and employee.\n\n**Required Scopes:** `read:epms_reviews`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "description": "Number of items per page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "scope",
            "in": "query",
            "description": "Filter by scope (mine, my_team, all)",
            "schema": {
              "type": "string",
              "enum": [
                "mine",
                "my_team",
                "all"
              ]
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by status",
            "schema": {
              "type": "string",
              "enum": [
                "created",
                "in_progress",
                "completed",
                "approved",
                "archived"
              ]
            }
          },
          {
            "name": "review_type",
            "in": "query",
            "description": "Filter by review type",
            "schema": {
              "type": "string",
              "enum": [
                "annual",
                "biannual",
                "quarterly",
                "probationary",
                "ad_hoc"
              ]
            }
          },
          {
            "name": "employee_id",
            "in": "query",
            "description": "Filter by employee ID",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "overdue",
            "in": "query",
            "description": "Filter overdue reviews",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Performance reviews retrieved successfully",
            "headers": {
              "X-Total-Count": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Total-Pages": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Current-Page": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Per-Page": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "Performance review with assessment details",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 234
                          },
                          "title": {
                            "type": "string",
                            "example": "Q1 2026 Performance Review"
                          },
                          "review_type": {
                            "type": "string",
                            "enum": [
                              "annual",
                              "quarterly",
                              "probation",
                              "project",
                              "performance_improvement"
                            ],
                            "example": "quarterly"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "created",
                              "in_progress",
                              "submitted",
                              "approved",
                              "completed",
                              "cancelled"
                            ],
                            "example": "in_progress"
                          },
                          "display_status": {
                            "type": "string",
                            "description": "Computed status that accounts for overdue reviews",
                            "example": "in_progress"
                          },
                          "status_color": {
                            "type": "string",
                            "description": "Bootstrap badge color variant for the display status",
                            "enum": [
                              "info",
                              "primary",
                              "success",
                              "danger",
                              "secondary"
                            ],
                            "example": "primary"
                          },
                          "review_period_start": {
                            "type": "string",
                            "format": "date",
                            "example": "2026-01-01"
                          },
                          "review_period_end": {
                            "type": "string",
                            "format": "date",
                            "example": "2026-03-31"
                          },
                          "due_date": {
                            "type": "string",
                            "format": "date",
                            "example": "2026-04-15"
                          },
                          "employee": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 456
                              },
                              "name": {
                                "type": "string",
                                "example": "John Doe"
                              }
                            }
                          },
                          "manager": {
                            "type": "object",
                            "nullable": true,
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 789
                              },
                              "name": {
                                "type": "string",
                                "example": "Jane Manager"
                              }
                            }
                          },
                          "overall_rating": {
                            "type": "number",
                            "nullable": true,
                            "minimum": 0,
                            "maximum": 5,
                            "example": 4.5
                          },
                          "manager_comments": {
                            "type": "string",
                            "nullable": true,
                            "example": "Great progress this quarter"
                          },
                          "self_assessment": {
                            "type": "object",
                            "nullable": true,
                            "properties": {
                              "overall_summary": {
                                "type": "string"
                              },
                              "strengths": {
                                "type": "string"
                              },
                              "areas_for_improvement": {
                                "type": "string"
                              },
                              "self_rating": {
                                "type": "number",
                                "minimum": 0,
                                "maximum": 5
                              }
                            }
                          },
                          "linked_goals": {
                            "type": "array",
                            "items": {
                              "$ref": "#/components/schemas/EPMSGoal"
                            }
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2026-01-01T10:00:00Z"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2026-01-15T14:30:00Z"
                          },
                          "is_overdue": {
                            "type": "boolean",
                            "example": false
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 6
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Items per page",
                          "example": 25
                        }
                      },
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "EPMS Performance Reviews"
        ],
        "summary": "Create performance review",
        "description": "Creates a new performance review. Managers only. Employee ID is required.\n\n**Required Scopes:** `write:epms_reviews`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "performance_review"
                ],
                "properties": {
                  "performance_review": {
                    "type": "object",
                    "description": "Input schema for creating/updating performance reviews",
                    "properties": {
                      "title": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 255,
                        "example": "Q1 2026 Performance Review"
                      },
                      "employee_id": {
                        "type": "integer",
                        "example": 456
                      },
                      "review_type": {
                        "type": "string",
                        "enum": [
                          "annual",
                          "quarterly",
                          "probation",
                          "project",
                          "performance_improvement"
                        ],
                        "example": "quarterly"
                      },
                      "review_period_start": {
                        "type": "string",
                        "format": "date",
                        "example": "2026-01-01"
                      },
                      "review_period_end": {
                        "type": "string",
                        "format": "date",
                        "example": "2026-03-31"
                      },
                      "due_date": {
                        "type": "string",
                        "format": "date",
                        "example": "2026-04-15"
                      },
                      "performance_review_template_id": {
                        "type": "integer",
                        "nullable": true
                      },
                      "manager_comments": {
                        "type": "string",
                        "maxLength": 5000,
                        "example": "Great progress this quarter"
                      },
                      "overall_rating": {
                        "type": "number",
                        "minimum": 0,
                        "maximum": 5,
                        "example": 4.5
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Performance review created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "performance_review": {
                      "type": "object",
                      "description": "Performance review with assessment details",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 234
                        },
                        "title": {
                          "type": "string",
                          "example": "Q1 2026 Performance Review"
                        },
                        "review_type": {
                          "type": "string",
                          "enum": [
                            "annual",
                            "quarterly",
                            "probation",
                            "project",
                            "performance_improvement"
                          ],
                          "example": "quarterly"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "created",
                            "in_progress",
                            "submitted",
                            "approved",
                            "completed",
                            "cancelled"
                          ],
                          "example": "in_progress"
                        },
                        "display_status": {
                          "type": "string",
                          "description": "Computed status that accounts for overdue reviews",
                          "example": "in_progress"
                        },
                        "status_color": {
                          "type": "string",
                          "description": "Bootstrap badge color variant for the display status",
                          "enum": [
                            "info",
                            "primary",
                            "success",
                            "danger",
                            "secondary"
                          ],
                          "example": "primary"
                        },
                        "review_period_start": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-01"
                        },
                        "review_period_end": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-03-31"
                        },
                        "due_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-04-15"
                        },
                        "employee": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            }
                          }
                        },
                        "manager": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            }
                          }
                        },
                        "overall_rating": {
                          "type": "number",
                          "nullable": true,
                          "minimum": 0,
                          "maximum": 5,
                          "example": 4.5
                        },
                        "manager_comments": {
                          "type": "string",
                          "nullable": true,
                          "example": "Great progress this quarter"
                        },
                        "self_assessment": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "overall_summary": {
                              "type": "string"
                            },
                            "strengths": {
                              "type": "string"
                            },
                            "areas_for_improvement": {
                              "type": "string"
                            },
                            "self_rating": {
                              "type": "number",
                              "minimum": 0,
                              "maximum": 5
                            }
                          }
                        },
                        "linked_goals": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/EPMSGoal"
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-01T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/performance_reviews/{id}": {
      "get": {
        "tags": [
          "EPMS Performance Reviews"
        ],
        "summary": "Get performance review details",
        "description": "Retrieves detailed performance review information. Can include linked goals and ratings.\n\n**Required Scopes:** `read:epms_reviews`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Performance review ID",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "include_goals",
            "in": "query",
            "description": "Include linked goals",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "include_ratings",
            "in": "query",
            "description": "Include ratings (if user has access)",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Performance review details retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "performance_review": {
                      "type": "object",
                      "description": "Performance review with assessment details",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 234
                        },
                        "title": {
                          "type": "string",
                          "example": "Q1 2026 Performance Review"
                        },
                        "review_type": {
                          "type": "string",
                          "enum": [
                            "annual",
                            "quarterly",
                            "probation",
                            "project",
                            "performance_improvement"
                          ],
                          "example": "quarterly"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "created",
                            "in_progress",
                            "submitted",
                            "approved",
                            "completed",
                            "cancelled"
                          ],
                          "example": "in_progress"
                        },
                        "display_status": {
                          "type": "string",
                          "description": "Computed status that accounts for overdue reviews",
                          "example": "in_progress"
                        },
                        "status_color": {
                          "type": "string",
                          "description": "Bootstrap badge color variant for the display status",
                          "enum": [
                            "info",
                            "primary",
                            "success",
                            "danger",
                            "secondary"
                          ],
                          "example": "primary"
                        },
                        "review_period_start": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-01"
                        },
                        "review_period_end": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-03-31"
                        },
                        "due_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-04-15"
                        },
                        "employee": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            }
                          }
                        },
                        "manager": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            }
                          }
                        },
                        "overall_rating": {
                          "type": "number",
                          "nullable": true,
                          "minimum": 0,
                          "maximum": 5,
                          "example": 4.5
                        },
                        "manager_comments": {
                          "type": "string",
                          "nullable": true,
                          "example": "Great progress this quarter"
                        },
                        "self_assessment": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "overall_summary": {
                              "type": "string"
                            },
                            "strengths": {
                              "type": "string"
                            },
                            "areas_for_improvement": {
                              "type": "string"
                            },
                            "self_rating": {
                              "type": "number",
                              "minimum": 0,
                              "maximum": 5
                            }
                          }
                        },
                        "linked_goals": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/EPMSGoal"
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-01T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "EPMS Performance Reviews"
        ],
        "summary": "Update performance review",
        "description": "Updates a performance review. Only certain fields can be updated based on review status.\n\n**Required Scopes:** `write:epms_reviews`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Performance review ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "performance_review"
                ],
                "properties": {
                  "performance_review": {
                    "type": "object",
                    "description": "Input schema for creating/updating performance reviews",
                    "properties": {
                      "title": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 255,
                        "example": "Q1 2026 Performance Review"
                      },
                      "employee_id": {
                        "type": "integer",
                        "example": 456
                      },
                      "review_type": {
                        "type": "string",
                        "enum": [
                          "annual",
                          "quarterly",
                          "probation",
                          "project",
                          "performance_improvement"
                        ],
                        "example": "quarterly"
                      },
                      "review_period_start": {
                        "type": "string",
                        "format": "date",
                        "example": "2026-01-01"
                      },
                      "review_period_end": {
                        "type": "string",
                        "format": "date",
                        "example": "2026-03-31"
                      },
                      "due_date": {
                        "type": "string",
                        "format": "date",
                        "example": "2026-04-15"
                      },
                      "performance_review_template_id": {
                        "type": "integer",
                        "nullable": true
                      },
                      "manager_comments": {
                        "type": "string",
                        "maxLength": 5000,
                        "example": "Great progress this quarter"
                      },
                      "overall_rating": {
                        "type": "number",
                        "minimum": 0,
                        "maximum": 5,
                        "example": 4.5
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Performance review updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "performance_review": {
                      "type": "object",
                      "description": "Performance review with assessment details",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 234
                        },
                        "title": {
                          "type": "string",
                          "example": "Q1 2026 Performance Review"
                        },
                        "review_type": {
                          "type": "string",
                          "enum": [
                            "annual",
                            "quarterly",
                            "probation",
                            "project",
                            "performance_improvement"
                          ],
                          "example": "quarterly"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "created",
                            "in_progress",
                            "submitted",
                            "approved",
                            "completed",
                            "cancelled"
                          ],
                          "example": "in_progress"
                        },
                        "display_status": {
                          "type": "string",
                          "description": "Computed status that accounts for overdue reviews",
                          "example": "in_progress"
                        },
                        "status_color": {
                          "type": "string",
                          "description": "Bootstrap badge color variant for the display status",
                          "enum": [
                            "info",
                            "primary",
                            "success",
                            "danger",
                            "secondary"
                          ],
                          "example": "primary"
                        },
                        "review_period_start": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-01"
                        },
                        "review_period_end": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-03-31"
                        },
                        "due_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-04-15"
                        },
                        "employee": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            }
                          }
                        },
                        "manager": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            }
                          }
                        },
                        "overall_rating": {
                          "type": "number",
                          "nullable": true,
                          "minimum": 0,
                          "maximum": 5,
                          "example": 4.5
                        },
                        "manager_comments": {
                          "type": "string",
                          "nullable": true,
                          "example": "Great progress this quarter"
                        },
                        "self_assessment": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "overall_summary": {
                              "type": "string"
                            },
                            "strengths": {
                              "type": "string"
                            },
                            "areas_for_improvement": {
                              "type": "string"
                            },
                            "self_rating": {
                              "type": "number",
                              "minimum": 0,
                              "maximum": 5
                            }
                          }
                        },
                        "linked_goals": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/EPMSGoal"
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-01T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "EPMS Performance Reviews"
        ],
        "summary": "Delete performance review",
        "description": "Deletes a performance review if allowed based on status and permissions.\n\n**Required Scopes:** `write:epms_reviews`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Performance review ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Performance review deleted successfully"
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/performance_reviews/{id}/submit": {
      "post": {
        "tags": [
          "EPMS Performance Reviews"
        ],
        "summary": "Submit performance review",
        "description": "Submits a performance review for approval. Review must be in progress status.\n\n**Required Scopes:** `write:epms_reviews`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Performance review ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Performance review submitted successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "performance_review": {
                      "type": "object",
                      "description": "Performance review with assessment details",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 234
                        },
                        "title": {
                          "type": "string",
                          "example": "Q1 2026 Performance Review"
                        },
                        "review_type": {
                          "type": "string",
                          "enum": [
                            "annual",
                            "quarterly",
                            "probation",
                            "project",
                            "performance_improvement"
                          ],
                          "example": "quarterly"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "created",
                            "in_progress",
                            "submitted",
                            "approved",
                            "completed",
                            "cancelled"
                          ],
                          "example": "in_progress"
                        },
                        "display_status": {
                          "type": "string",
                          "description": "Computed status that accounts for overdue reviews",
                          "example": "in_progress"
                        },
                        "status_color": {
                          "type": "string",
                          "description": "Bootstrap badge color variant for the display status",
                          "enum": [
                            "info",
                            "primary",
                            "success",
                            "danger",
                            "secondary"
                          ],
                          "example": "primary"
                        },
                        "review_period_start": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-01"
                        },
                        "review_period_end": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-03-31"
                        },
                        "due_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-04-15"
                        },
                        "employee": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            }
                          }
                        },
                        "manager": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            }
                          }
                        },
                        "overall_rating": {
                          "type": "number",
                          "nullable": true,
                          "minimum": 0,
                          "maximum": 5,
                          "example": 4.5
                        },
                        "manager_comments": {
                          "type": "string",
                          "nullable": true,
                          "example": "Great progress this quarter"
                        },
                        "self_assessment": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "overall_summary": {
                              "type": "string"
                            },
                            "strengths": {
                              "type": "string"
                            },
                            "areas_for_improvement": {
                              "type": "string"
                            },
                            "self_rating": {
                              "type": "number",
                              "minimum": 0,
                              "maximum": 5
                            }
                          }
                        },
                        "linked_goals": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/EPMSGoal"
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-01T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        }
                      }
                    },
                    "message": {
                      "type": "string",
                      "example": "Performance review has been submitted for approval"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/performance_reviews/{id}/approve": {
      "post": {
        "tags": [
          "EPMS Performance Reviews"
        ],
        "summary": "Approve performance review",
        "description": "Approves a submitted performance review. Requires manager or admin permissions.\n\n**Required Scopes:** `write:epms_reviews`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Performance review ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Performance review approved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "performance_review": {
                      "type": "object",
                      "description": "Performance review with assessment details",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 234
                        },
                        "title": {
                          "type": "string",
                          "example": "Q1 2026 Performance Review"
                        },
                        "review_type": {
                          "type": "string",
                          "enum": [
                            "annual",
                            "quarterly",
                            "probation",
                            "project",
                            "performance_improvement"
                          ],
                          "example": "quarterly"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "created",
                            "in_progress",
                            "submitted",
                            "approved",
                            "completed",
                            "cancelled"
                          ],
                          "example": "in_progress"
                        },
                        "display_status": {
                          "type": "string",
                          "description": "Computed status that accounts for overdue reviews",
                          "example": "in_progress"
                        },
                        "status_color": {
                          "type": "string",
                          "description": "Bootstrap badge color variant for the display status",
                          "enum": [
                            "info",
                            "primary",
                            "success",
                            "danger",
                            "secondary"
                          ],
                          "example": "primary"
                        },
                        "review_period_start": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-01"
                        },
                        "review_period_end": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-03-31"
                        },
                        "due_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-04-15"
                        },
                        "employee": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            }
                          }
                        },
                        "manager": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            }
                          }
                        },
                        "overall_rating": {
                          "type": "number",
                          "nullable": true,
                          "minimum": 0,
                          "maximum": 5,
                          "example": 4.5
                        },
                        "manager_comments": {
                          "type": "string",
                          "nullable": true,
                          "example": "Great progress this quarter"
                        },
                        "self_assessment": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "overall_summary": {
                              "type": "string"
                            },
                            "strengths": {
                              "type": "string"
                            },
                            "areas_for_improvement": {
                              "type": "string"
                            },
                            "self_rating": {
                              "type": "number",
                              "minimum": 0,
                              "maximum": 5
                            }
                          }
                        },
                        "linked_goals": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/EPMSGoal"
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-01T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        }
                      }
                    },
                    "message": {
                      "type": "string",
                      "example": "Performance review has been approved"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/performance_reviews/{id}/archive": {
      "post": {
        "tags": [
          "EPMS Performance Reviews"
        ],
        "summary": "Archive performance review",
        "description": "Archives a completed or approved performance review — the recoverable\nalternative to deletion. Requires the review's reviewer or HR/admin\npermissions.\n\n**Required Scopes:** `write:epms_reviews`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Performance review ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Performance review archived successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "performance_review": {
                      "type": "object",
                      "description": "Performance review with assessment details",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 234
                        },
                        "title": {
                          "type": "string",
                          "example": "Q1 2026 Performance Review"
                        },
                        "review_type": {
                          "type": "string",
                          "enum": [
                            "annual",
                            "quarterly",
                            "probation",
                            "project",
                            "performance_improvement"
                          ],
                          "example": "quarterly"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "created",
                            "in_progress",
                            "submitted",
                            "approved",
                            "completed",
                            "cancelled"
                          ],
                          "example": "in_progress"
                        },
                        "display_status": {
                          "type": "string",
                          "description": "Computed status that accounts for overdue reviews",
                          "example": "in_progress"
                        },
                        "status_color": {
                          "type": "string",
                          "description": "Bootstrap badge color variant for the display status",
                          "enum": [
                            "info",
                            "primary",
                            "success",
                            "danger",
                            "secondary"
                          ],
                          "example": "primary"
                        },
                        "review_period_start": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-01"
                        },
                        "review_period_end": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-03-31"
                        },
                        "due_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-04-15"
                        },
                        "employee": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            }
                          }
                        },
                        "manager": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            }
                          }
                        },
                        "overall_rating": {
                          "type": "number",
                          "nullable": true,
                          "minimum": 0,
                          "maximum": 5,
                          "example": 4.5
                        },
                        "manager_comments": {
                          "type": "string",
                          "nullable": true,
                          "example": "Great progress this quarter"
                        },
                        "self_assessment": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "overall_summary": {
                              "type": "string"
                            },
                            "strengths": {
                              "type": "string"
                            },
                            "areas_for_improvement": {
                              "type": "string"
                            },
                            "self_rating": {
                              "type": "number",
                              "minimum": 0,
                              "maximum": 5
                            }
                          }
                        },
                        "linked_goals": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/EPMSGoal"
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-01T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/performance_reviews/{id}/self_assessment": {
      "post": {
        "tags": [
          "EPMS Performance Reviews"
        ],
        "summary": "Submit self assessment",
        "description": "Submits employee self-assessment for a performance review. Includes overall summary, strengths, areas for improvement, and ratings.\n\n**Required Scopes:** `write:epms_reviews`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Performance review ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "self_assessment"
                ],
                "properties": {
                  "self_assessment": {
                    "type": "object",
                    "description": "Input schema for self-assessment submission",
                    "properties": {
                      "overall_summary": {
                        "type": "string",
                        "maxLength": 2000,
                        "example": "I achieved all my goals this quarter"
                      },
                      "strengths": {
                        "type": "string",
                        "maxLength": 1000,
                        "example": "Strong communication and leadership"
                      },
                      "areas_for_improvement": {
                        "type": "string",
                        "maxLength": 1000,
                        "example": "Time management"
                      },
                      "accomplishments": {
                        "type": "string",
                        "maxLength": 2000,
                        "example": "Completed 3 major projects"
                      },
                      "goals_reflection": {
                        "type": "string",
                        "maxLength": 1000,
                        "example": "Met 90% of goals"
                      },
                      "training_needs": {
                        "type": "string",
                        "maxLength": 500,
                        "example": "Advanced project management"
                      },
                      "career_aspirations": {
                        "type": "string",
                        "maxLength": 1000,
                        "example": "Lead a larger team"
                      },
                      "self_rating": {
                        "type": "number",
                        "minimum": 0,
                        "maximum": 5,
                        "example": 4.0
                      },
                      "ratings": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "category": {
                              "type": "string",
                              "example": "Communication"
                            },
                            "rating": {
                              "type": "number",
                              "minimum": 0,
                              "maximum": 5,
                              "example": 4.5
                            },
                            "comments": {
                              "type": "string",
                              "example": "Excellent"
                            }
                          }
                        }
                      },
                      "goals": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "goal_id": {
                              "type": "integer",
                              "example": 123
                            },
                            "achievement_notes": {
                              "type": "string",
                              "example": "Exceeded expectations"
                            },
                            "self_rating": {
                              "type": "number",
                              "minimum": 0,
                              "maximum": 5,
                              "example": 5.0
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Self assessment submitted successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "performance_review": {
                      "type": "object",
                      "description": "Performance review with assessment details",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 234
                        },
                        "title": {
                          "type": "string",
                          "example": "Q1 2026 Performance Review"
                        },
                        "review_type": {
                          "type": "string",
                          "enum": [
                            "annual",
                            "quarterly",
                            "probation",
                            "project",
                            "performance_improvement"
                          ],
                          "example": "quarterly"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "created",
                            "in_progress",
                            "submitted",
                            "approved",
                            "completed",
                            "cancelled"
                          ],
                          "example": "in_progress"
                        },
                        "display_status": {
                          "type": "string",
                          "description": "Computed status that accounts for overdue reviews",
                          "example": "in_progress"
                        },
                        "status_color": {
                          "type": "string",
                          "description": "Bootstrap badge color variant for the display status",
                          "enum": [
                            "info",
                            "primary",
                            "success",
                            "danger",
                            "secondary"
                          ],
                          "example": "primary"
                        },
                        "review_period_start": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-01"
                        },
                        "review_period_end": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-03-31"
                        },
                        "due_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-04-15"
                        },
                        "employee": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            }
                          }
                        },
                        "manager": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            }
                          }
                        },
                        "overall_rating": {
                          "type": "number",
                          "nullable": true,
                          "minimum": 0,
                          "maximum": 5,
                          "example": 4.5
                        },
                        "manager_comments": {
                          "type": "string",
                          "nullable": true,
                          "example": "Great progress this quarter"
                        },
                        "self_assessment": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "overall_summary": {
                              "type": "string"
                            },
                            "strengths": {
                              "type": "string"
                            },
                            "areas_for_improvement": {
                              "type": "string"
                            },
                            "self_rating": {
                              "type": "number",
                              "minimum": 0,
                              "maximum": 5
                            }
                          }
                        },
                        "linked_goals": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/EPMSGoal"
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-01T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        }
                      }
                    },
                    "message": {
                      "type": "string",
                      "example": "Self assessment has been submitted"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/performance_reviews/{id}/link_goals": {
      "post": {
        "tags": [
          "EPMS Performance Reviews"
        ],
        "summary": "Link goals to review",
        "description": "Links goals to a performance review for evaluation.\n\n**Required Scopes:** `write:epms_reviews`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Performance review ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "goal_ids"
                ],
                "properties": {
                  "goal_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "Array of goal IDs to link"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Goals linked successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "performance_review": {
                      "type": "object",
                      "description": "Performance review with assessment details",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 234
                        },
                        "title": {
                          "type": "string",
                          "example": "Q1 2026 Performance Review"
                        },
                        "review_type": {
                          "type": "string",
                          "enum": [
                            "annual",
                            "quarterly",
                            "probation",
                            "project",
                            "performance_improvement"
                          ],
                          "example": "quarterly"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "created",
                            "in_progress",
                            "submitted",
                            "approved",
                            "completed",
                            "cancelled"
                          ],
                          "example": "in_progress"
                        },
                        "display_status": {
                          "type": "string",
                          "description": "Computed status that accounts for overdue reviews",
                          "example": "in_progress"
                        },
                        "status_color": {
                          "type": "string",
                          "description": "Bootstrap badge color variant for the display status",
                          "enum": [
                            "info",
                            "primary",
                            "success",
                            "danger",
                            "secondary"
                          ],
                          "example": "primary"
                        },
                        "review_period_start": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-01"
                        },
                        "review_period_end": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-03-31"
                        },
                        "due_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-04-15"
                        },
                        "employee": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            }
                          }
                        },
                        "manager": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            }
                          }
                        },
                        "overall_rating": {
                          "type": "number",
                          "nullable": true,
                          "minimum": 0,
                          "maximum": 5,
                          "example": 4.5
                        },
                        "manager_comments": {
                          "type": "string",
                          "nullable": true,
                          "example": "Great progress this quarter"
                        },
                        "self_assessment": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "overall_summary": {
                              "type": "string"
                            },
                            "strengths": {
                              "type": "string"
                            },
                            "areas_for_improvement": {
                              "type": "string"
                            },
                            "self_rating": {
                              "type": "number",
                              "minimum": 0,
                              "maximum": 5
                            }
                          }
                        },
                        "linked_goals": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/EPMSGoal"
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-01T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        }
                      }
                    },
                    "message": {
                      "type": "string",
                      "example": "Goals have been linked to the performance review"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/performance_reviews/stats": {
      "get": {
        "tags": [
          "EPMS Performance Reviews"
        ],
        "summary": "Get performance review statistics",
        "description": "Retrieves performance review statistics. Can be filtered by employee ID.\nAlso returns `my_review_summary` with the current user's personal review\nsummary matching the \"My Review Status\" widget on the web dashboard for\nsingle-employee contexts (`scope=mine` or default scope without aggregate views).\n\n**Required Scopes:** `read:epms_reviews`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "employee_id",
            "in": "query",
            "description": "Optional filter by employee ID",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "scope",
            "in": "query",
            "description": "Optional scope filter. Aggregate scopes omit `my_review_summary`.",
            "schema": {
              "type": "string",
              "enum": [
                "mine",
                "my_team",
                "all"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Performance review statistics retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "stats": {
                      "type": "object",
                      "description": "Performance review statistics",
                      "properties": {
                        "total_reviews": {
                          "type": "integer",
                          "example": 15
                        },
                        "pending_reviews": {
                          "type": "integer",
                          "example": 5
                        },
                        "completed_reviews": {
                          "type": "integer",
                          "example": 8
                        },
                        "overdue_reviews": {
                          "type": "integer",
                          "example": 2
                        },
                        "due_soon_count": {
                          "type": "integer",
                          "example": 3
                        },
                        "completion_rate": {
                          "type": "number",
                          "example": 53.3
                        },
                        "by_status": {
                          "type": "object",
                          "description": "Count of reviews by status key",
                          "additionalProperties": {
                            "type": "integer"
                          },
                          "example": {
                            "created": 2,
                            "in_progress": 5,
                            "completed": 3,
                            "approved": 2,
                            "archived": 1
                          }
                        },
                        "by_type": {
                          "type": "object",
                          "description": "Count of reviews by review_type key",
                          "additionalProperties": {
                            "type": "integer"
                          },
                          "example": {
                            "annual": 8,
                            "quarterly": 5,
                            "ad_hoc": 2
                          }
                        },
                        "average_rating": {
                          "type": "number",
                          "nullable": true,
                          "example": 4.2
                        }
                      }
                    },
                    "my_review_summary": {
                      "type": "object",
                      "description": "Personal review summary for the current user (matches \"My Review Status\" widget)",
                      "properties": {
                        "total_reviews": {
                          "type": "integer",
                          "description": "Total number of reviews where the user is the employee",
                          "example": 5
                        },
                        "completed_reviews": {
                          "type": "integer",
                          "description": "Reviews with status completed or approved",
                          "example": 3
                        },
                        "pending_reviews": {
                          "type": "integer",
                          "description": "Reviews with status created or in_progress",
                          "example": 1
                        },
                        "average_rating": {
                          "type": "number",
                          "description": "Average overall_rating across completed/approved reviews (rounded to 1 decimal, defaults to 0)",
                          "example": 4.2
                        },
                        "has_active_review": {
                          "type": "boolean",
                          "description": "Whether the user has an active (created or in_progress) review",
                          "example": true
                        },
                        "current_review": {
                          "type": "object",
                          "nullable": true,
                          "description": "The most recent active review summary",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "title": {
                              "type": "string"
                            },
                            "status": {
                              "type": "string"
                            },
                            "display_status": {
                              "type": "string"
                            },
                            "status_color": {
                              "type": "string",
                              "enum": [
                                "info",
                                "primary",
                                "success",
                                "danger",
                                "secondary"
                              ]
                            },
                            "start_date": {
                              "type": "string",
                              "format": "date",
                              "nullable": true
                            },
                            "due_date": {
                              "type": "string",
                              "format": "date",
                              "nullable": true
                            },
                            "days_until_due": {
                              "type": "integer",
                              "nullable": true
                            },
                            "completion_percentage": {
                              "type": "integer"
                            },
                            "requires_self_assessment": {
                              "type": "boolean"
                            },
                            "self_assessment_completed": {
                              "type": "boolean"
                            }
                          }
                        },
                        "pending_actions": {
                          "type": "array",
                          "description": "Status-based action items for the user",
                          "items": {
                            "type": "object",
                            "properties": {
                              "type": {
                                "type": "string",
                                "enum": [
                                  "begin_review",
                                  "complete_self_assessment",
                                  "awaiting_approval"
                                ]
                              },
                              "text": {
                                "type": "string"
                              },
                              "priority": {
                                "type": "string",
                                "enum": [
                                  "high",
                                  "medium",
                                  "low"
                                ]
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/continuous_feedbacks": {
      "get": {
        "tags": [
          "EPMS Continuous Feedback"
        ],
        "summary": "List feedback",
        "description": "Retrieves a list of feedback with filtering options. Supports filtering by scope, status, type, visibility, and receiver.\n\n**Required Scopes:** `read:epms_feedback`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "description": "Number of items per page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "scope",
            "in": "query",
            "description": "Filter by scope (given, received, team, all)",
            "schema": {
              "type": "string",
              "enum": [
                "given",
                "received",
                "team",
                "all"
              ]
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by status",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "sent",
                "acknowledged",
                "archived"
              ]
            }
          },
          {
            "name": "feedback_type",
            "in": "query",
            "description": "Filter by feedback type",
            "schema": {
              "type": "string",
              "enum": [
                "praise",
                "constructive",
                "goal_progress",
                "general"
              ]
            }
          },
          {
            "name": "visibility",
            "in": "query",
            "description": "Filter by visibility",
            "schema": {
              "type": "string",
              "enum": [
                "private",
                "manager",
                "hr",
                "public"
              ]
            }
          },
          {
            "name": "receiver_id",
            "in": "query",
            "description": "Filter by receiver ID",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "pending_acknowledgment",
            "in": "query",
            "description": "Filter pending acknowledgment",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Feedback retrieved successfully",
            "headers": {
              "X-Total-Count": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Total-Pages": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Current-Page": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Per-Page": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "Continuous feedback record",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 345
                          },
                          "subject": {
                            "type": "string",
                            "example": "Great work on the project"
                          },
                          "content": {
                            "type": "string",
                            "example": "Your presentation was excellent and well-received by the team."
                          },
                          "feedback_type": {
                            "type": "string",
                            "enum": [
                              "praise",
                              "constructive",
                              "goal_progress",
                              "general"
                            ],
                            "example": "praise"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "draft",
                              "sent",
                              "acknowledged",
                              "archived"
                            ],
                            "example": "sent"
                          },
                          "visibility": {
                            "type": "string",
                            "enum": [
                              "private",
                              "public",
                              "manager_only"
                            ],
                            "example": "private"
                          },
                          "is_anonymous": {
                            "type": "boolean",
                            "example": false
                          },
                          "giver": {
                            "type": "object",
                            "nullable": true,
                            "description": "Feedback giver info. Returns only `{ name: \"Anonymous\" }` when feedback is anonymous.",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 789
                              },
                              "name": {
                                "type": "string",
                                "example": "Jane Manager"
                              },
                              "email": {
                                "type": "string",
                                "format": "email",
                                "example": "jane.manager@company.com"
                              },
                              "job_title": {
                                "type": "string",
                                "nullable": true,
                                "example": "Engineering Manager"
                              },
                              "profile_photo_url": {
                                "type": "string",
                                "format": "uri",
                                "nullable": true,
                                "description": "Full-size profile photo URL (200x200)",
                                "example": "https://example.com/photos/jane-200x200.jpg"
                              },
                              "profile_photo_thumbnail_url": {
                                "type": "string",
                                "format": "uri",
                                "nullable": true,
                                "description": "Thumbnail profile photo URL (40x40)",
                                "example": "https://example.com/photos/jane-40x40.jpg"
                              }
                            }
                          },
                          "receiver": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 456
                              },
                              "name": {
                                "type": "string",
                                "example": "John Doe"
                              },
                              "email": {
                                "type": "string",
                                "format": "email",
                                "example": "john.doe@company.com"
                              },
                              "job_title": {
                                "type": "string",
                                "nullable": true,
                                "example": "Senior Product Manager"
                              },
                              "profile_photo_url": {
                                "type": "string",
                                "format": "uri",
                                "nullable": true,
                                "description": "Full-size profile photo URL (200x200)",
                                "example": "https://example.com/photos/john-200x200.jpg"
                              },
                              "profile_photo_thumbnail_url": {
                                "type": "string",
                                "format": "uri",
                                "nullable": true,
                                "description": "Thumbnail profile photo URL (40x40)",
                                "example": "https://example.com/photos/john-40x40.jpg"
                              }
                            }
                          },
                          "acknowledged_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "example": "2026-01-20T10:00:00Z"
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2026-01-15T14:30:00Z"
                          },
                          "sent_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "example": "2026-01-15T15:00:00Z"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 6
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Items per page",
                          "example": 25
                        }
                      },
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "EPMS Continuous Feedback"
        ],
        "summary": "Create feedback (draft)",
        "description": "Creates new feedback. Starts as draft status. Must be sent separately.\n\n**Required Scopes:** `write:epms_feedback`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "feedback"
                ],
                "properties": {
                  "feedback": {
                    "type": "object",
                    "description": "Input schema for creating/updating feedback",
                    "required": [
                      "receiver_id",
                      "subject",
                      "content"
                    ],
                    "properties": {
                      "receiver_id": {
                        "type": "integer",
                        "example": 456
                      },
                      "subject": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 255,
                        "example": "Great work on the project"
                      },
                      "content": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 2000,
                        "example": "Your presentation was excellent and well-received by the team."
                      },
                      "feedback_type": {
                        "type": "string",
                        "enum": [
                          "praise",
                          "constructive",
                          "goal_progress",
                          "general"
                        ],
                        "default": "praise",
                        "example": "praise"
                      },
                      "visibility": {
                        "type": "string",
                        "enum": [
                          "private",
                          "public",
                          "manager_only"
                        ],
                        "default": "private",
                        "example": "private"
                      },
                      "is_anonymous": {
                        "type": "boolean",
                        "default": false,
                        "example": false
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Feedback created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "feedback": {
                      "type": "object",
                      "description": "Continuous feedback record",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 345
                        },
                        "subject": {
                          "type": "string",
                          "example": "Great work on the project"
                        },
                        "content": {
                          "type": "string",
                          "example": "Your presentation was excellent and well-received by the team."
                        },
                        "feedback_type": {
                          "type": "string",
                          "enum": [
                            "praise",
                            "constructive",
                            "goal_progress",
                            "general"
                          ],
                          "example": "praise"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "sent",
                            "acknowledged",
                            "archived"
                          ],
                          "example": "sent"
                        },
                        "visibility": {
                          "type": "string",
                          "enum": [
                            "private",
                            "public",
                            "manager_only"
                          ],
                          "example": "private"
                        },
                        "is_anonymous": {
                          "type": "boolean",
                          "example": false
                        },
                        "giver": {
                          "type": "object",
                          "nullable": true,
                          "description": "Feedback giver info. Returns only `{ name: \"Anonymous\" }` when feedback is anonymous.",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            },
                            "email": {
                              "type": "string",
                              "format": "email",
                              "example": "jane.manager@company.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true,
                              "example": "Engineering Manager"
                            },
                            "profile_photo_url": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200)",
                              "example": "https://example.com/photos/jane-200x200.jpg"
                            },
                            "profile_photo_thumbnail_url": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40)",
                              "example": "https://example.com/photos/jane-40x40.jpg"
                            }
                          }
                        },
                        "receiver": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            },
                            "email": {
                              "type": "string",
                              "format": "email",
                              "example": "john.doe@company.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true,
                              "example": "Senior Product Manager"
                            },
                            "profile_photo_url": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200)",
                              "example": "https://example.com/photos/john-200x200.jpg"
                            },
                            "profile_photo_thumbnail_url": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40)",
                              "example": "https://example.com/photos/john-40x40.jpg"
                            }
                          }
                        },
                        "acknowledged_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "example": "2026-01-20T10:00:00Z"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        },
                        "sent_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "example": "2026-01-15T15:00:00Z"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/continuous_feedbacks/{id}": {
      "get": {
        "tags": [
          "EPMS Continuous Feedback"
        ],
        "summary": "Get feedback details",
        "description": "Retrieves detailed feedback information.\n\n**Required Scopes:** `read:epms_feedback`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Feedback ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Feedback details retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "feedback": {
                      "type": "object",
                      "description": "Continuous feedback record",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 345
                        },
                        "subject": {
                          "type": "string",
                          "example": "Great work on the project"
                        },
                        "content": {
                          "type": "string",
                          "example": "Your presentation was excellent and well-received by the team."
                        },
                        "feedback_type": {
                          "type": "string",
                          "enum": [
                            "praise",
                            "constructive",
                            "goal_progress",
                            "general"
                          ],
                          "example": "praise"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "sent",
                            "acknowledged",
                            "archived"
                          ],
                          "example": "sent"
                        },
                        "visibility": {
                          "type": "string",
                          "enum": [
                            "private",
                            "public",
                            "manager_only"
                          ],
                          "example": "private"
                        },
                        "is_anonymous": {
                          "type": "boolean",
                          "example": false
                        },
                        "giver": {
                          "type": "object",
                          "nullable": true,
                          "description": "Feedback giver info. Returns only `{ name: \"Anonymous\" }` when feedback is anonymous.",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            },
                            "email": {
                              "type": "string",
                              "format": "email",
                              "example": "jane.manager@company.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true,
                              "example": "Engineering Manager"
                            },
                            "profile_photo_url": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200)",
                              "example": "https://example.com/photos/jane-200x200.jpg"
                            },
                            "profile_photo_thumbnail_url": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40)",
                              "example": "https://example.com/photos/jane-40x40.jpg"
                            }
                          }
                        },
                        "receiver": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            },
                            "email": {
                              "type": "string",
                              "format": "email",
                              "example": "john.doe@company.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true,
                              "example": "Senior Product Manager"
                            },
                            "profile_photo_url": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200)",
                              "example": "https://example.com/photos/john-200x200.jpg"
                            },
                            "profile_photo_thumbnail_url": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40)",
                              "example": "https://example.com/photos/john-40x40.jpg"
                            }
                          }
                        },
                        "acknowledged_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "example": "2026-01-20T10:00:00Z"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        },
                        "sent_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "example": "2026-01-15T15:00:00Z"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "EPMS Continuous Feedback"
        ],
        "summary": "Update feedback",
        "description": "Updates feedback. Only drafts can be edited by the giver.\n\n**Required Scopes:** `write:epms_feedback`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Feedback ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "feedback"
                ],
                "properties": {
                  "feedback": {
                    "type": "object",
                    "description": "Input schema for creating/updating feedback",
                    "required": [
                      "receiver_id",
                      "subject",
                      "content"
                    ],
                    "properties": {
                      "receiver_id": {
                        "type": "integer",
                        "example": 456
                      },
                      "subject": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 255,
                        "example": "Great work on the project"
                      },
                      "content": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 2000,
                        "example": "Your presentation was excellent and well-received by the team."
                      },
                      "feedback_type": {
                        "type": "string",
                        "enum": [
                          "praise",
                          "constructive",
                          "goal_progress",
                          "general"
                        ],
                        "default": "praise",
                        "example": "praise"
                      },
                      "visibility": {
                        "type": "string",
                        "enum": [
                          "private",
                          "public",
                          "manager_only"
                        ],
                        "default": "private",
                        "example": "private"
                      },
                      "is_anonymous": {
                        "type": "boolean",
                        "default": false,
                        "example": false
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Feedback updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "feedback": {
                      "type": "object",
                      "description": "Continuous feedback record",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 345
                        },
                        "subject": {
                          "type": "string",
                          "example": "Great work on the project"
                        },
                        "content": {
                          "type": "string",
                          "example": "Your presentation was excellent and well-received by the team."
                        },
                        "feedback_type": {
                          "type": "string",
                          "enum": [
                            "praise",
                            "constructive",
                            "goal_progress",
                            "general"
                          ],
                          "example": "praise"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "sent",
                            "acknowledged",
                            "archived"
                          ],
                          "example": "sent"
                        },
                        "visibility": {
                          "type": "string",
                          "enum": [
                            "private",
                            "public",
                            "manager_only"
                          ],
                          "example": "private"
                        },
                        "is_anonymous": {
                          "type": "boolean",
                          "example": false
                        },
                        "giver": {
                          "type": "object",
                          "nullable": true,
                          "description": "Feedback giver info. Returns only `{ name: \"Anonymous\" }` when feedback is anonymous.",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            },
                            "email": {
                              "type": "string",
                              "format": "email",
                              "example": "jane.manager@company.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true,
                              "example": "Engineering Manager"
                            },
                            "profile_photo_url": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200)",
                              "example": "https://example.com/photos/jane-200x200.jpg"
                            },
                            "profile_photo_thumbnail_url": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40)",
                              "example": "https://example.com/photos/jane-40x40.jpg"
                            }
                          }
                        },
                        "receiver": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            },
                            "email": {
                              "type": "string",
                              "format": "email",
                              "example": "john.doe@company.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true,
                              "example": "Senior Product Manager"
                            },
                            "profile_photo_url": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200)",
                              "example": "https://example.com/photos/john-200x200.jpg"
                            },
                            "profile_photo_thumbnail_url": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40)",
                              "example": "https://example.com/photos/john-40x40.jpg"
                            }
                          }
                        },
                        "acknowledged_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "example": "2026-01-20T10:00:00Z"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        },
                        "sent_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "example": "2026-01-15T15:00:00Z"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "EPMS Continuous Feedback"
        ],
        "summary": "Delete feedback",
        "description": "Deletes feedback. Only drafts can be deleted by the giver.\n\n**Required Scopes:** `write:epms_feedback`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Feedback ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Feedback deleted successfully"
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/continuous_feedbacks/{id}/send": {
      "post": {
        "tags": [
          "EPMS Continuous Feedback"
        ],
        "summary": "Send feedback",
        "description": "Sends the feedback to the receiver. Changes status from draft to sent.\n\n**Required Scopes:** `write:epms_feedback`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Feedback ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Feedback sent successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "feedback": {
                      "type": "object",
                      "description": "Continuous feedback record",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 345
                        },
                        "subject": {
                          "type": "string",
                          "example": "Great work on the project"
                        },
                        "content": {
                          "type": "string",
                          "example": "Your presentation was excellent and well-received by the team."
                        },
                        "feedback_type": {
                          "type": "string",
                          "enum": [
                            "praise",
                            "constructive",
                            "goal_progress",
                            "general"
                          ],
                          "example": "praise"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "sent",
                            "acknowledged",
                            "archived"
                          ],
                          "example": "sent"
                        },
                        "visibility": {
                          "type": "string",
                          "enum": [
                            "private",
                            "public",
                            "manager_only"
                          ],
                          "example": "private"
                        },
                        "is_anonymous": {
                          "type": "boolean",
                          "example": false
                        },
                        "giver": {
                          "type": "object",
                          "nullable": true,
                          "description": "Feedback giver info. Returns only `{ name: \"Anonymous\" }` when feedback is anonymous.",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            },
                            "email": {
                              "type": "string",
                              "format": "email",
                              "example": "jane.manager@company.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true,
                              "example": "Engineering Manager"
                            },
                            "profile_photo_url": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200)",
                              "example": "https://example.com/photos/jane-200x200.jpg"
                            },
                            "profile_photo_thumbnail_url": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40)",
                              "example": "https://example.com/photos/jane-40x40.jpg"
                            }
                          }
                        },
                        "receiver": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            },
                            "email": {
                              "type": "string",
                              "format": "email",
                              "example": "john.doe@company.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true,
                              "example": "Senior Product Manager"
                            },
                            "profile_photo_url": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200)",
                              "example": "https://example.com/photos/john-200x200.jpg"
                            },
                            "profile_photo_thumbnail_url": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40)",
                              "example": "https://example.com/photos/john-40x40.jpg"
                            }
                          }
                        },
                        "acknowledged_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "example": "2026-01-20T10:00:00Z"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        },
                        "sent_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "example": "2026-01-15T15:00:00Z"
                        }
                      }
                    },
                    "message": {
                      "type": "string",
                      "example": "Feedback has been sent"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/continuous_feedbacks/{id}/acknowledge": {
      "post": {
        "tags": [
          "EPMS Continuous Feedback"
        ],
        "summary": "Acknowledge feedback",
        "description": "Acknowledges received feedback. Changes status from sent to acknowledged.\n\n**Required Scopes:** `write:epms_feedback`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Feedback ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Feedback acknowledged successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "feedback": {
                      "type": "object",
                      "description": "Continuous feedback record",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 345
                        },
                        "subject": {
                          "type": "string",
                          "example": "Great work on the project"
                        },
                        "content": {
                          "type": "string",
                          "example": "Your presentation was excellent and well-received by the team."
                        },
                        "feedback_type": {
                          "type": "string",
                          "enum": [
                            "praise",
                            "constructive",
                            "goal_progress",
                            "general"
                          ],
                          "example": "praise"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "sent",
                            "acknowledged",
                            "archived"
                          ],
                          "example": "sent"
                        },
                        "visibility": {
                          "type": "string",
                          "enum": [
                            "private",
                            "public",
                            "manager_only"
                          ],
                          "example": "private"
                        },
                        "is_anonymous": {
                          "type": "boolean",
                          "example": false
                        },
                        "giver": {
                          "type": "object",
                          "nullable": true,
                          "description": "Feedback giver info. Returns only `{ name: \"Anonymous\" }` when feedback is anonymous.",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            },
                            "email": {
                              "type": "string",
                              "format": "email",
                              "example": "jane.manager@company.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true,
                              "example": "Engineering Manager"
                            },
                            "profile_photo_url": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200)",
                              "example": "https://example.com/photos/jane-200x200.jpg"
                            },
                            "profile_photo_thumbnail_url": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40)",
                              "example": "https://example.com/photos/jane-40x40.jpg"
                            }
                          }
                        },
                        "receiver": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            },
                            "email": {
                              "type": "string",
                              "format": "email",
                              "example": "john.doe@company.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true,
                              "example": "Senior Product Manager"
                            },
                            "profile_photo_url": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200)",
                              "example": "https://example.com/photos/john-200x200.jpg"
                            },
                            "profile_photo_thumbnail_url": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40)",
                              "example": "https://example.com/photos/john-40x40.jpg"
                            }
                          }
                        },
                        "acknowledged_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "example": "2026-01-20T10:00:00Z"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        },
                        "sent_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "example": "2026-01-15T15:00:00Z"
                        }
                      }
                    },
                    "message": {
                      "type": "string",
                      "example": "Feedback has been acknowledged"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/continuous_feedbacks/{id}/archive": {
      "post": {
        "tags": [
          "EPMS Continuous Feedback"
        ],
        "summary": "Archive feedback",
        "description": "Archives feedback. Changes status to archived.\n\n**Required Scopes:** `write:epms_feedback`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Feedback ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Feedback archived successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "feedback": {
                      "type": "object",
                      "description": "Continuous feedback record",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 345
                        },
                        "subject": {
                          "type": "string",
                          "example": "Great work on the project"
                        },
                        "content": {
                          "type": "string",
                          "example": "Your presentation was excellent and well-received by the team."
                        },
                        "feedback_type": {
                          "type": "string",
                          "enum": [
                            "praise",
                            "constructive",
                            "goal_progress",
                            "general"
                          ],
                          "example": "praise"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "sent",
                            "acknowledged",
                            "archived"
                          ],
                          "example": "sent"
                        },
                        "visibility": {
                          "type": "string",
                          "enum": [
                            "private",
                            "public",
                            "manager_only"
                          ],
                          "example": "private"
                        },
                        "is_anonymous": {
                          "type": "boolean",
                          "example": false
                        },
                        "giver": {
                          "type": "object",
                          "nullable": true,
                          "description": "Feedback giver info. Returns only `{ name: \"Anonymous\" }` when feedback is anonymous.",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            },
                            "email": {
                              "type": "string",
                              "format": "email",
                              "example": "jane.manager@company.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true,
                              "example": "Engineering Manager"
                            },
                            "profile_photo_url": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200)",
                              "example": "https://example.com/photos/jane-200x200.jpg"
                            },
                            "profile_photo_thumbnail_url": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40)",
                              "example": "https://example.com/photos/jane-40x40.jpg"
                            }
                          }
                        },
                        "receiver": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            },
                            "email": {
                              "type": "string",
                              "format": "email",
                              "example": "john.doe@company.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true,
                              "example": "Senior Product Manager"
                            },
                            "profile_photo_url": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true,
                              "description": "Full-size profile photo URL (200x200)",
                              "example": "https://example.com/photos/john-200x200.jpg"
                            },
                            "profile_photo_thumbnail_url": {
                              "type": "string",
                              "format": "uri",
                              "nullable": true,
                              "description": "Thumbnail profile photo URL (40x40)",
                              "example": "https://example.com/photos/john-40x40.jpg"
                            }
                          }
                        },
                        "acknowledged_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "example": "2026-01-20T10:00:00Z"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        },
                        "sent_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "example": "2026-01-15T15:00:00Z"
                        }
                      }
                    },
                    "message": {
                      "type": "string",
                      "example": "Feedback has been archived"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/continuous_feedbacks/stats": {
      "get": {
        "tags": [
          "EPMS Continuous Feedback"
        ],
        "summary": "Get feedback statistics",
        "description": "Retrieves feedback statistics. Can be filtered by scope.\n\n**Required Scopes:** `read:epms_feedback`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "scope",
            "in": "query",
            "description": "Filter by scope (given, received)",
            "schema": {
              "type": "string",
              "enum": [
                "given",
                "received"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Feedback statistics retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "stats": {
                      "type": "object",
                      "description": "Feedback statistics",
                      "properties": {
                        "given": {
                          "type": "object",
                          "properties": {
                            "total": {
                              "type": "integer",
                              "example": 25
                            },
                            "by_type": {
                              "type": "object",
                              "properties": {
                                "praise": {
                                  "type": "integer",
                                  "example": 15
                                },
                                "constructive": {
                                  "type": "integer",
                                  "example": 5
                                },
                                "recognition": {
                                  "type": "integer",
                                  "example": 3
                                },
                                "coaching": {
                                  "type": "integer",
                                  "example": 2
                                }
                              }
                            }
                          }
                        },
                        "received": {
                          "type": "object",
                          "properties": {
                            "total": {
                              "type": "integer",
                              "example": 18
                            },
                            "pending_acknowledgment": {
                              "type": "integer",
                              "example": 3
                            },
                            "acknowledged": {
                              "type": "integer",
                              "example": 15
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/continuous_feedbacks/user_picker": {
      "get": {
        "tags": [
          "EPMS Continuous Feedback"
        ],
        "summary": "User picker for feedback giving / receiving",
        "description": "Returns a paginated, searchable list of eligible users for the mobile\nfeedback user-picker, scoped by the requested mode.\n\n| Mode | Returns |\n|------|---------|\n| `giving` | All active business members except the current user — anyone can receive feedback |\n| `receiving` | Users whose received-feedback the current user may view: **HR/Admin** → all users; **Managers** → themselves + direct reports; **Employees** → all active users (for filtering their own inbox) |\n\n**Required Scopes:** `read:epms_feedback`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "mode",
            "in": "query",
            "required": true,
            "description": "Picker mode:\n- `giving` — users the current user can give feedback **to**\n- `receiving` — users whose received feedback the current user can view\n",
            "schema": {
              "type": "string",
              "enum": [
                "giving",
                "receiving"
              ]
            },
            "example": "giving"
          },
          {
            "name": "search",
            "in": "query",
            "description": "Search by full name or email address (case-insensitive)",
            "schema": {
              "type": "string",
              "example": "Jane"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "description": "Number of items per page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Eligible users retrieved successfully",
            "headers": {
              "X-Total-Count": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Total-Pages": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Current-Page": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Per-Page": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "Lightweight user object returned by the feedback user-picker endpoint.\nUsed for selecting recipients (giving mode) or viewing feedback targets (receiving mode).\n",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "description": "User's internal ID",
                            "example": 456
                          },
                          "name": {
                            "type": "string",
                            "description": "User's full name",
                            "example": "Jane Smith"
                          },
                          "email": {
                            "type": "string",
                            "format": "email",
                            "nullable": true,
                            "description": "User's email address",
                            "example": "jane.smith@company.com"
                          },
                          "job_title": {
                            "type": "string",
                            "nullable": true,
                            "description": "User's job title",
                            "example": "Senior Engineer"
                          },
                          "department": {
                            "type": "string",
                            "nullable": true,
                            "description": "User's department name",
                            "example": "Engineering"
                          },
                          "avatar_url": {
                            "type": "string",
                            "format": "uri",
                            "nullable": true,
                            "description": "Full-size profile photo URL (200×200 px)",
                            "example": "https://example.com/photos/jane-200x200.jpg"
                          },
                          "avatar_thumbnail_url": {
                            "type": "string",
                            "format": "uri",
                            "nullable": true,
                            "description": "Thumbnail profile photo URL (40×40 px)",
                            "example": "https://example.com/photos/jane-40x40.jpg"
                          }
                        },
                        "required": [
                          "id",
                          "name"
                        ]
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 6
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Items per page",
                          "example": 25
                        }
                      },
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ]
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": 456,
                      "name": "Jane Smith",
                      "email": "jane.smith@company.com",
                      "job_title": "Senior Engineer",
                      "department": "Engineering",
                      "avatar_url": "https://example.com/photos/jane-200x200.jpg",
                      "avatar_thumbnail_url": "https://example.com/photos/jane-40x40.jpg"
                    },
                    {
                      "id": 789,
                      "name": "John Doe",
                      "email": "john.doe@company.com",
                      "job_title": "Product Manager",
                      "department": "Product",
                      "avatar_url": null,
                      "avatar_thumbnail_url": null
                    }
                  ],
                  "meta": {
                    "total_count": 42,
                    "total_pages": 2,
                    "current_page": 1,
                    "per_page": 25,
                    "has_next_page": true,
                    "has_prev_page": false
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid mode parameter",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "invalid_mode"
                        },
                        "message": {
                          "type": "string",
                          "example": "Invalid mode 'foo'. Valid values are: giving, receiving"
                        },
                        "details": {
                          "type": "object",
                          "properties": {
                            "valid_modes": {
                              "type": "array",
                              "items": {
                                "type": "string"
                              },
                              "example": [
                                "giving",
                                "receiving"
                              ]
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/development_plans": {
      "get": {
        "tags": [
          "EPMS Development Plans"
        ],
        "summary": "List development plans",
        "description": "Retrieves a list of development plans with filtering options.\n\n**Required Scopes:** `read:epms_development`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "description": "Number of items per page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "scope",
            "in": "query",
            "description": "Filter by scope (mine, team)",
            "schema": {
              "type": "string",
              "enum": [
                "mine",
                "team"
              ]
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by status",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "active",
                "on_hold",
                "completed",
                "cancelled"
              ]
            }
          },
          {
            "name": "employee_id",
            "in": "query",
            "description": "Filter by employee ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Development plans retrieved successfully",
            "headers": {
              "X-Total-Count": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Total-Pages": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Current-Page": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Per-Page": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "Employee development plan",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 456
                          },
                          "title": {
                            "type": "string",
                            "example": "Leadership Development Plan"
                          },
                          "description": {
                            "type": "string",
                            "nullable": true,
                            "example": "Focus on developing leadership skills"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "draft",
                              "active",
                              "completed",
                              "cancelled"
                            ],
                            "example": "active"
                          },
                          "start_date": {
                            "type": "string",
                            "format": "date",
                            "example": "2026-01-01"
                          },
                          "target_completion_date": {
                            "type": "string",
                            "format": "date",
                            "nullable": true,
                            "example": "2026-12-31"
                          },
                          "plan_type": {
                            "type": "string",
                            "nullable": true,
                            "example": "leadership"
                          },
                          "employee": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 456
                              },
                              "name": {
                                "type": "string",
                                "example": "John Doe"
                              }
                            }
                          },
                          "manager": {
                            "type": "object",
                            "nullable": true,
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 789
                              },
                              "name": {
                                "type": "string",
                                "example": "Jane Manager"
                              }
                            }
                          },
                          "goals": {
                            "type": "array",
                            "items": {
                              "$ref": "#/components/schemas/EPMSGoal"
                            }
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2026-01-01T10:00:00Z"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2026-01-15T14:30:00Z"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 6
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Items per page",
                          "example": 25
                        }
                      },
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "EPMS Development Plans"
        ],
        "summary": "Create development plan",
        "description": "Creates a new development plan.\n\n**Required Scopes:** `write:epms_development`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "development_plan"
                ],
                "properties": {
                  "development_plan": {
                    "type": "object",
                    "description": "Input schema for creating/updating development plans",
                    "required": [
                      "title"
                    ],
                    "properties": {
                      "title": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 255,
                        "example": "Leadership Development Plan"
                      },
                      "description": {
                        "type": "string",
                        "maxLength": 2000,
                        "example": "Focus on developing leadership skills"
                      },
                      "status": {
                        "type": "string",
                        "enum": [
                          "draft",
                          "active",
                          "completed",
                          "cancelled"
                        ],
                        "example": "active"
                      },
                      "start_date": {
                        "type": "string",
                        "format": "date",
                        "example": "2026-01-01"
                      },
                      "target_completion_date": {
                        "type": "string",
                        "format": "date",
                        "nullable": true,
                        "example": "2026-12-31"
                      },
                      "employee_id": {
                        "type": "integer",
                        "example": 456
                      },
                      "plan_type": {
                        "type": "string",
                        "example": "leadership"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Development plan created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "development_plan": {
                      "type": "object",
                      "description": "Employee development plan",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 456
                        },
                        "title": {
                          "type": "string",
                          "example": "Leadership Development Plan"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "example": "Focus on developing leadership skills"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "active",
                            "completed",
                            "cancelled"
                          ],
                          "example": "active"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-01"
                        },
                        "target_completion_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "example": "2026-12-31"
                        },
                        "plan_type": {
                          "type": "string",
                          "nullable": true,
                          "example": "leadership"
                        },
                        "employee": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            }
                          }
                        },
                        "manager": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            }
                          }
                        },
                        "goals": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/EPMSGoal"
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-01T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/development_plans/{id}": {
      "get": {
        "tags": [
          "EPMS Development Plans"
        ],
        "summary": "Get development plan details",
        "description": "Retrieves detailed development plan information. Can include development goals.\n\n**Required Scopes:** `read:epms_development`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Development plan ID",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "include_goals",
            "in": "query",
            "description": "Include development goals",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Development plan details retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "development_plan": {
                      "type": "object",
                      "description": "Employee development plan",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 456
                        },
                        "title": {
                          "type": "string",
                          "example": "Leadership Development Plan"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "example": "Focus on developing leadership skills"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "active",
                            "completed",
                            "cancelled"
                          ],
                          "example": "active"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-01"
                        },
                        "target_completion_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "example": "2026-12-31"
                        },
                        "plan_type": {
                          "type": "string",
                          "nullable": true,
                          "example": "leadership"
                        },
                        "employee": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            }
                          }
                        },
                        "manager": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            }
                          }
                        },
                        "goals": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/EPMSGoal"
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-01T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "EPMS Development Plans"
        ],
        "summary": "Update development plan",
        "description": "Updates a development plan.\n\n**Required Scopes:** `write:epms_development`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Development plan ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "development_plan"
                ],
                "properties": {
                  "development_plan": {
                    "type": "object",
                    "description": "Input schema for creating/updating development plans",
                    "required": [
                      "title"
                    ],
                    "properties": {
                      "title": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 255,
                        "example": "Leadership Development Plan"
                      },
                      "description": {
                        "type": "string",
                        "maxLength": 2000,
                        "example": "Focus on developing leadership skills"
                      },
                      "status": {
                        "type": "string",
                        "enum": [
                          "draft",
                          "active",
                          "completed",
                          "cancelled"
                        ],
                        "example": "active"
                      },
                      "start_date": {
                        "type": "string",
                        "format": "date",
                        "example": "2026-01-01"
                      },
                      "target_completion_date": {
                        "type": "string",
                        "format": "date",
                        "nullable": true,
                        "example": "2026-12-31"
                      },
                      "employee_id": {
                        "type": "integer",
                        "example": 456
                      },
                      "plan_type": {
                        "type": "string",
                        "example": "leadership"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Development plan updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "development_plan": {
                      "type": "object",
                      "description": "Employee development plan",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 456
                        },
                        "title": {
                          "type": "string",
                          "example": "Leadership Development Plan"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "example": "Focus on developing leadership skills"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "active",
                            "completed",
                            "cancelled"
                          ],
                          "example": "active"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-01"
                        },
                        "target_completion_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "example": "2026-12-31"
                        },
                        "plan_type": {
                          "type": "string",
                          "nullable": true,
                          "example": "leadership"
                        },
                        "employee": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            }
                          }
                        },
                        "manager": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            }
                          }
                        },
                        "goals": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/EPMSGoal"
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-01T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "EPMS Development Plans"
        ],
        "summary": "Delete development plan",
        "description": "Deletes a development plan if allowed based on status and permissions.\n\n**Required Scopes:** `write:epms_development`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Development plan ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Development plan deleted successfully"
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/development_plans/{id}/goals": {
      "post": {
        "tags": [
          "EPMS Development Plans"
        ],
        "summary": "Add goal to development plan",
        "description": "Adds a development goal to the plan.\n\n**Required Scopes:** `write:epms_development`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Development plan ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "goal"
                ],
                "properties": {
                  "goal": {
                    "type": "object",
                    "description": "Input schema for development plan goals",
                    "required": [
                      "title",
                      "target_date"
                    ],
                    "properties": {
                      "title": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 255,
                        "example": "Complete Leadership Training"
                      },
                      "description": {
                        "type": "string",
                        "maxLength": 2000,
                        "example": "Finish the leadership certification program"
                      },
                      "goal_type": {
                        "type": "string",
                        "enum": [
                          "skill",
                          "experience",
                          "education",
                          "certification",
                          "project"
                        ],
                        "example": "skill"
                      },
                      "status": {
                        "type": "string",
                        "enum": [
                          "not_started",
                          "in_progress",
                          "completed",
                          "cancelled"
                        ],
                        "example": "not_started"
                      },
                      "target_date": {
                        "type": "string",
                        "format": "date",
                        "example": "2026-06-30"
                      },
                      "progress_percentage": {
                        "type": "number",
                        "minimum": 0,
                        "maximum": 100,
                        "default": 0,
                        "example": 0
                      },
                      "priority": {
                        "type": "string",
                        "enum": [
                          "low",
                          "medium",
                          "high",
                          "critical"
                        ],
                        "example": "high"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Goal added successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "goal": {
                      "type": "object",
                      "description": "Employee goal with progress tracking. Progress updates are returned as a sibling array in GET /goals/{id}, not nested in the goal object.",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 123
                        },
                        "title": {
                          "type": "string",
                          "example": "Increase sales by 20%"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "example": "Achieve 20% growth in Q1 sales"
                        },
                        "goal_type": {
                          "type": "string",
                          "enum": [
                            "performance",
                            "development",
                            "behavior",
                            "project",
                            "skill"
                          ],
                          "example": "performance"
                        },
                        "goal_type_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for goal type"
                        },
                        "goal_category": {
                          "type": "string",
                          "nullable": true,
                          "description": "The focus area category key for this goal",
                          "example": "professional_development"
                        },
                        "goal_category_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for the goal category",
                          "example": "Professional Development"
                        },
                        "priority": {
                          "type": "string",
                          "enum": [
                            "low",
                            "medium",
                            "high",
                            "critical"
                          ],
                          "example": "high"
                        },
                        "priority_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable label for priority"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "in_review",
                            "active",
                            "on_hold",
                            "completed",
                            "cancelled",
                            "overdue"
                          ],
                          "example": "active"
                        },
                        "display_status": {
                          "type": "string",
                          "nullable": true,
                          "description": "Human-readable display status"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-01"
                        },
                        "target_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-03-31"
                        },
                        "completed_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "progress_percentage": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 100,
                          "example": 45.5
                        },
                        "weight_percentage": {
                          "type": "number",
                          "nullable": true,
                          "example": 30.0
                        },
                        "workflow_stage": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "employee_review",
                            "manager_finalized",
                            "leadership_approved"
                          ],
                          "description": "Current workflow stage"
                        },
                        "workflow_stage_label": {
                          "type": "string",
                          "nullable": true
                        },
                        "is_smart_goal": {
                          "type": "boolean",
                          "example": true
                        },
                        "smart_score": {
                          "type": "number",
                          "nullable": true
                        },
                        "on_track": {
                          "type": "boolean",
                          "nullable": true
                        },
                        "days_until_due": {
                          "type": "integer",
                          "nullable": true
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        },
                        "progress_update_allowed": {
                          "type": "boolean",
                          "description": "Whether progress updates can be submitted for this goal"
                        },
                        "success_criteria": {
                          "type": "string",
                          "nullable": true,
                          "description": "Present when include_details is true (e.g. show endpoint)",
                          "example": "Reach $500K in sales"
                        },
                        "smart_criteria": {
                          "type": "object",
                          "nullable": true,
                          "description": "Present when include_details is true",
                          "properties": {
                            "is_specific": {
                              "type": "boolean"
                            },
                            "is_measurable": {
                              "type": "boolean"
                            },
                            "is_achievable": {
                              "type": "boolean"
                            },
                            "is_relevant": {
                              "type": "boolean"
                            },
                            "is_time_bound": {
                              "type": "boolean"
                            }
                          }
                        },
                        "progress_updates_count": {
                          "type": "integer",
                          "description": "Present when include_details is true"
                        },
                        "latest_progress_update": {
                          "$ref": "#/components/schemas/EPMSProgressUpdate",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_complete": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_delete": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "requires_manager_approval": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "is_fully_approved": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "in_review_stage": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_cancel": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_put_on_hold": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_reactivate": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_send_to_employee": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_employee_review": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_manager_finalize": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "can_leadership_approve": {
                          "type": "boolean",
                          "description": "Present when include_details is true"
                        },
                        "manager_approved_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "leadership_approved_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Present when include_details is true"
                        },
                        "employee": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "john@example.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-01T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        }
                      }
                    },
                    "development_plan": {
                      "type": "object",
                      "description": "Employee development plan",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 456
                        },
                        "title": {
                          "type": "string",
                          "example": "Leadership Development Plan"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "example": "Focus on developing leadership skills"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "active",
                            "completed",
                            "cancelled"
                          ],
                          "example": "active"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-01"
                        },
                        "target_completion_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "example": "2026-12-31"
                        },
                        "plan_type": {
                          "type": "string",
                          "nullable": true,
                          "example": "leadership"
                        },
                        "employee": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            }
                          }
                        },
                        "manager": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            }
                          }
                        },
                        "goals": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/EPMSGoal"
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-01T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/meetings": {
      "get": {
        "tags": [
          "EPMS Meetings"
        ],
        "summary": "List meetings",
        "description": "Retrieves a list of meetings (1:1s, check-ins) with filtering options.\n\n**Required Scopes:** `read:epms_meetings`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "description": "Number of items per page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "scope",
            "in": "query",
            "description": "Filter by scope (mine, managed, team, all)",
            "schema": {
              "type": "string",
              "enum": [
                "mine",
                "managed",
                "team",
                "all"
              ]
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by status",
            "schema": {
              "type": "string",
              "enum": [
                "scheduled",
                "confirmed",
                "in_progress",
                "completed",
                "cancelled",
                "rescheduled"
              ]
            }
          },
          {
            "name": "meeting_type",
            "in": "query",
            "description": "Filter by meeting type",
            "schema": {
              "type": "string",
              "enum": [
                "performance_review",
                "goal_discussion",
                "feedback_session",
                "check_in",
                "development_planning",
                "career_discussion"
              ]
            }
          },
          {
            "name": "employee_id",
            "in": "query",
            "description": "Filter by employee ID",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "upcoming",
            "in": "query",
            "description": "Filter upcoming meetings only",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Meetings retrieved successfully",
            "headers": {
              "X-Total-Count": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Total-Pages": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Current-Page": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Per-Page": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "One-on-one meeting or check-in",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 567
                          },
                          "title": {
                            "type": "string",
                            "example": "Q1 Check-in"
                          },
                          "meeting_type": {
                            "type": "string",
                            "enum": [
                              "performance_review",
                              "goal_discussion",
                              "feedback_session",
                              "check_in",
                              "development_planning",
                              "career_discussion"
                            ],
                            "example": "check_in"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "scheduled",
                              "confirmed",
                              "in_progress",
                              "completed",
                              "cancelled",
                              "rescheduled"
                            ],
                            "example": "scheduled"
                          },
                          "scheduled_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2026-02-15T10:00:00Z"
                          },
                          "duration_minutes": {
                            "type": "integer",
                            "example": 30
                          },
                          "location": {
                            "type": "string",
                            "nullable": true,
                            "example": "Conference Room A"
                          },
                          "description": {
                            "type": "string",
                            "nullable": true,
                            "example": "Quarterly performance check-in"
                          },
                          "employee": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 456
                              },
                              "name": {
                                "type": "string",
                                "example": "John Doe"
                              }
                            }
                          },
                          "manager": {
                            "type": "object",
                            "nullable": true,
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 789
                              },
                              "name": {
                                "type": "string",
                                "example": "Jane Manager"
                              }
                            }
                          },
                          "notes": {
                            "type": "string",
                            "nullable": true,
                            "example": "Great discussion about goals and progress"
                          },
                          "completed_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "example": "2026-02-15T10:30:00Z"
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2026-01-15T10:00:00Z"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2026-01-15T10:00:00Z"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 6
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Items per page",
                          "example": 25
                        }
                      },
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "EPMS Meetings"
        ],
        "summary": "Create meeting",
        "description": "Schedules a new meeting (1:1 or check-in).\n\n**Required Scopes:** `write:epms_meetings`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "meeting"
                ],
                "properties": {
                  "meeting": {
                    "type": "object",
                    "description": "Input schema for creating/updating meetings",
                    "required": [
                      "title",
                      "meeting_type",
                      "employee_id",
                      "scheduled_at"
                    ],
                    "properties": {
                      "title": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 255,
                        "example": "Q1 Check-in"
                      },
                      "meeting_type": {
                        "type": "string",
                        "enum": [
                          "performance_review",
                          "goal_discussion",
                          "feedback_session",
                          "check_in",
                          "development_planning",
                          "career_discussion"
                        ],
                        "example": "check_in"
                      },
                      "employee_id": {
                        "type": "integer",
                        "example": 456
                      },
                      "scheduled_at": {
                        "type": "string",
                        "format": "date-time",
                        "example": "2026-02-15T10:00:00Z"
                      },
                      "duration_minutes": {
                        "type": "integer",
                        "default": 30,
                        "example": 30
                      },
                      "location": {
                        "type": "string",
                        "maxLength": 255,
                        "example": "Conference Room A"
                      },
                      "description": {
                        "type": "string",
                        "maxLength": 2000,
                        "example": "Quarterly performance check-in"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Meeting created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "meeting": {
                      "type": "object",
                      "description": "One-on-one meeting or check-in",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 567
                        },
                        "title": {
                          "type": "string",
                          "example": "Q1 Check-in"
                        },
                        "meeting_type": {
                          "type": "string",
                          "enum": [
                            "performance_review",
                            "goal_discussion",
                            "feedback_session",
                            "check_in",
                            "development_planning",
                            "career_discussion"
                          ],
                          "example": "check_in"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "scheduled",
                            "confirmed",
                            "in_progress",
                            "completed",
                            "cancelled",
                            "rescheduled"
                          ],
                          "example": "scheduled"
                        },
                        "scheduled_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-02-15T10:00:00Z"
                        },
                        "duration_minutes": {
                          "type": "integer",
                          "example": 30
                        },
                        "location": {
                          "type": "string",
                          "nullable": true,
                          "example": "Conference Room A"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "example": "Quarterly performance check-in"
                        },
                        "employee": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            }
                          }
                        },
                        "manager": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            }
                          }
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true,
                          "example": "Great discussion about goals and progress"
                        },
                        "completed_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "example": "2026-02-15T10:30:00Z"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T10:00:00Z"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/meetings/{id}": {
      "get": {
        "tags": [
          "EPMS Meetings"
        ],
        "summary": "Get meeting details",
        "description": "Retrieves detailed meeting information.\n\n**Required Scopes:** `read:epms_meetings`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Meeting ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Meeting details retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "meeting": {
                      "type": "object",
                      "description": "One-on-one meeting or check-in",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 567
                        },
                        "title": {
                          "type": "string",
                          "example": "Q1 Check-in"
                        },
                        "meeting_type": {
                          "type": "string",
                          "enum": [
                            "performance_review",
                            "goal_discussion",
                            "feedback_session",
                            "check_in",
                            "development_planning",
                            "career_discussion"
                          ],
                          "example": "check_in"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "scheduled",
                            "confirmed",
                            "in_progress",
                            "completed",
                            "cancelled",
                            "rescheduled"
                          ],
                          "example": "scheduled"
                        },
                        "scheduled_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-02-15T10:00:00Z"
                        },
                        "duration_minutes": {
                          "type": "integer",
                          "example": 30
                        },
                        "location": {
                          "type": "string",
                          "nullable": true,
                          "example": "Conference Room A"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "example": "Quarterly performance check-in"
                        },
                        "employee": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            }
                          }
                        },
                        "manager": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            }
                          }
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true,
                          "example": "Great discussion about goals and progress"
                        },
                        "completed_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "example": "2026-02-15T10:30:00Z"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T10:00:00Z"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "EPMS Meetings"
        ],
        "summary": "Update meeting",
        "description": "Updates a meeting.\n\n**Required Scopes:** `write:epms_meetings`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Meeting ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "meeting"
                ],
                "properties": {
                  "meeting": {
                    "type": "object",
                    "description": "Input schema for creating/updating meetings",
                    "required": [
                      "title",
                      "meeting_type",
                      "employee_id",
                      "scheduled_at"
                    ],
                    "properties": {
                      "title": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 255,
                        "example": "Q1 Check-in"
                      },
                      "meeting_type": {
                        "type": "string",
                        "enum": [
                          "performance_review",
                          "goal_discussion",
                          "feedback_session",
                          "check_in",
                          "development_planning",
                          "career_discussion"
                        ],
                        "example": "check_in"
                      },
                      "employee_id": {
                        "type": "integer",
                        "example": 456
                      },
                      "scheduled_at": {
                        "type": "string",
                        "format": "date-time",
                        "example": "2026-02-15T10:00:00Z"
                      },
                      "duration_minutes": {
                        "type": "integer",
                        "default": 30,
                        "example": 30
                      },
                      "location": {
                        "type": "string",
                        "maxLength": 255,
                        "example": "Conference Room A"
                      },
                      "description": {
                        "type": "string",
                        "maxLength": 2000,
                        "example": "Quarterly performance check-in"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Meeting updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "meeting": {
                      "type": "object",
                      "description": "One-on-one meeting or check-in",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 567
                        },
                        "title": {
                          "type": "string",
                          "example": "Q1 Check-in"
                        },
                        "meeting_type": {
                          "type": "string",
                          "enum": [
                            "performance_review",
                            "goal_discussion",
                            "feedback_session",
                            "check_in",
                            "development_planning",
                            "career_discussion"
                          ],
                          "example": "check_in"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "scheduled",
                            "confirmed",
                            "in_progress",
                            "completed",
                            "cancelled",
                            "rescheduled"
                          ],
                          "example": "scheduled"
                        },
                        "scheduled_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-02-15T10:00:00Z"
                        },
                        "duration_minutes": {
                          "type": "integer",
                          "example": 30
                        },
                        "location": {
                          "type": "string",
                          "nullable": true,
                          "example": "Conference Room A"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "example": "Quarterly performance check-in"
                        },
                        "employee": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            }
                          }
                        },
                        "manager": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            }
                          }
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true,
                          "example": "Great discussion about goals and progress"
                        },
                        "completed_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "example": "2026-02-15T10:30:00Z"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T10:00:00Z"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "EPMS Meetings"
        ],
        "summary": "Delete meeting",
        "description": "Cancels/deletes a meeting.\n\n**Required Scopes:** `write:epms_meetings`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Meeting ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Meeting deleted successfully"
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/meetings/{id}/complete": {
      "post": {
        "tags": [
          "EPMS Meetings"
        ],
        "summary": "Complete meeting",
        "description": "Marks a meeting as complete with optional notes.\n\n**Required Scopes:** `write:epms_meetings`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Meeting ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "notes": {
                    "type": "string",
                    "description": "Meeting completion notes"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Meeting completed successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "meeting": {
                      "type": "object",
                      "description": "One-on-one meeting or check-in",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 567
                        },
                        "title": {
                          "type": "string",
                          "example": "Q1 Check-in"
                        },
                        "meeting_type": {
                          "type": "string",
                          "enum": [
                            "performance_review",
                            "goal_discussion",
                            "feedback_session",
                            "check_in",
                            "development_planning",
                            "career_discussion"
                          ],
                          "example": "check_in"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "scheduled",
                            "confirmed",
                            "in_progress",
                            "completed",
                            "cancelled",
                            "rescheduled"
                          ],
                          "example": "scheduled"
                        },
                        "scheduled_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-02-15T10:00:00Z"
                        },
                        "duration_minutes": {
                          "type": "integer",
                          "example": 30
                        },
                        "location": {
                          "type": "string",
                          "nullable": true,
                          "example": "Conference Room A"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "example": "Quarterly performance check-in"
                        },
                        "employee": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            }
                          }
                        },
                        "manager": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            }
                          }
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true,
                          "example": "Great discussion about goals and progress"
                        },
                        "completed_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "example": "2026-02-15T10:30:00Z"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T10:00:00Z"
                        }
                      }
                    },
                    "message": {
                      "type": "string",
                      "example": "Meeting has been completed"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/competency_frameworks": {
      "get": {
        "tags": [
          "EPMS Competency Frameworks"
        ],
        "summary": "List competency frameworks",
        "description": "Retrieves a list of competency frameworks. Read-only for most users.\n\n**Required Scopes:** `read:epms_competencies`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "description": "Number of items per page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "framework_type",
            "in": "query",
            "description": "Filter by framework type",
            "schema": {
              "type": "string",
              "enum": [
                "core",
                "leadership",
                "technical",
                "functional",
                "role_specific"
              ]
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Search by name or description",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Competency frameworks retrieved successfully",
            "headers": {
              "X-Total-Count": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Total-Pages": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Current-Page": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Per-Page": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "Competency framework with defined competencies",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 678
                          },
                          "name": {
                            "type": "string",
                            "example": "Leadership Competencies"
                          },
                          "description": {
                            "type": "string",
                            "nullable": true,
                            "example": "Core leadership competencies for management roles"
                          },
                          "framework_type": {
                            "type": "string",
                            "enum": [
                              "core",
                              "leadership",
                              "technical",
                              "functional",
                              "role_specific"
                            ],
                            "example": "leadership"
                          },
                          "competencies": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "type": "integer",
                                  "example": 1
                                },
                                "name": {
                                  "type": "string",
                                  "example": "Communication"
                                },
                                "description": {
                                  "type": "string",
                                  "nullable": true,
                                  "example": "Ability to communicate effectively"
                                },
                                "level": {
                                  "type": "string",
                                  "enum": [
                                    "beginner",
                                    "intermediate",
                                    "advanced",
                                    "expert"
                                  ],
                                  "nullable": true
                                }
                              }
                            }
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2026-01-01T10:00:00Z"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2026-01-01T10:00:00Z"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 6
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Items per page",
                          "example": 25
                        }
                      },
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/competency_frameworks/{id}": {
      "get": {
        "tags": [
          "EPMS Competency Frameworks"
        ],
        "summary": "Get competency framework details",
        "description": "Retrieves detailed framework information with competencies.\n\n**Required Scopes:** `read:epms_competencies`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Competency framework ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Competency framework details retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "competency_framework": {
                      "type": "object",
                      "description": "Competency framework with defined competencies",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 678
                        },
                        "name": {
                          "type": "string",
                          "example": "Leadership Competencies"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "example": "Core leadership competencies for management roles"
                        },
                        "framework_type": {
                          "type": "string",
                          "enum": [
                            "core",
                            "leadership",
                            "technical",
                            "functional",
                            "role_specific"
                          ],
                          "example": "leadership"
                        },
                        "competencies": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 1
                              },
                              "name": {
                                "type": "string",
                                "example": "Communication"
                              },
                              "description": {
                                "type": "string",
                                "nullable": true,
                                "example": "Ability to communicate effectively"
                              },
                              "level": {
                                "type": "string",
                                "enum": [
                                  "beginner",
                                  "intermediate",
                                  "advanced",
                                  "expert"
                                ],
                                "nullable": true
                              }
                            }
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-01T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-01T10:00:00Z"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/competency_assessments": {
      "get": {
        "tags": [
          "EPMS Competency Assessments"
        ],
        "summary": "List competency assessments",
        "description": "Retrieves a list of competency assessments with filtering options.\n\n**Required Scopes:** `read:epms_competencies`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "description": "Number of items per page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "scope",
            "in": "query",
            "description": "Filter by scope (mine, team)",
            "schema": {
              "type": "string",
              "enum": [
                "mine",
                "team"
              ]
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by status",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "in_progress",
                "completed",
                "approved",
                "cancelled"
              ]
            }
          },
          {
            "name": "employee_id",
            "in": "query",
            "description": "Filter by employee ID",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "competency_framework_id",
            "in": "query",
            "description": "Filter by framework ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Competency assessments retrieved successfully",
            "headers": {
              "X-Total-Count": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Total-Pages": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Current-Page": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Per-Page": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "Competency assessment with ratings",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 789
                          },
                          "employee": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 456
                              },
                              "name": {
                                "type": "string",
                                "example": "John Doe"
                              }
                            }
                          },
                          "competency_framework": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 678
                              },
                              "name": {
                                "type": "string",
                                "example": "Leadership Competencies"
                              }
                            }
                          },
                          "assessment_type": {
                            "type": "string",
                            "enum": [
                              "self_assessment",
                              "manager_assessment",
                              "peer_assessment",
                              "360_assessment"
                            ],
                            "example": "manager_assessment"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "draft",
                              "in_progress",
                              "completed"
                            ],
                            "example": "completed"
                          },
                          "assessed_at": {
                            "type": "string",
                            "format": "date",
                            "example": "2026-01-15"
                          },
                          "due_date": {
                            "type": "string",
                            "format": "date",
                            "nullable": true,
                            "example": "2026-01-31"
                          },
                          "overall_score": {
                            "type": "number",
                            "nullable": true,
                            "minimum": 0,
                            "maximum": 5,
                            "example": 4.2
                          },
                          "overall_comments": {
                            "type": "string",
                            "nullable": true,
                            "example": "Strong performance across all competencies"
                          },
                          "competency_ratings": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "competency_id": {
                                  "type": "integer",
                                  "example": 1
                                },
                                "competency_name": {
                                  "type": "string",
                                  "example": "Communication"
                                },
                                "rating_value": {
                                  "type": "number",
                                  "minimum": 0,
                                  "maximum": 5,
                                  "example": 4.5
                                },
                                "comments": {
                                  "type": "string",
                                  "nullable": true,
                                  "example": "Excellent communication skills"
                                }
                              }
                            }
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2026-01-15T10:00:00Z"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2026-01-15T14:30:00Z"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 6
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Items per page",
                          "example": 25
                        }
                      },
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "EPMS Competency Assessments"
        ],
        "summary": "Create competency assessment",
        "description": "Creates a new competency assessment with ratings.\n\n**Required Scopes:** `write:epms_competencies`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "competency_assessment"
                ],
                "properties": {
                  "competency_assessment": {
                    "type": "object",
                    "description": "Input schema for creating/updating competency assessments",
                    "required": [
                      "employee_id",
                      "competency_framework_id"
                    ],
                    "properties": {
                      "employee_id": {
                        "type": "integer",
                        "example": 456
                      },
                      "competency_framework_id": {
                        "type": "integer",
                        "example": 678
                      },
                      "assessment_type": {
                        "type": "string",
                        "enum": [
                          "self_assessment",
                          "manager_assessment",
                          "peer_assessment",
                          "360_assessment"
                        ],
                        "example": "manager_assessment"
                      },
                      "assessed_at": {
                        "type": "string",
                        "format": "date",
                        "example": "2026-01-15"
                      },
                      "due_date": {
                        "type": "string",
                        "format": "date",
                        "nullable": true,
                        "example": "2026-01-31"
                      },
                      "overall_score": {
                        "type": "number",
                        "minimum": 0,
                        "maximum": 5,
                        "example": 4.2
                      },
                      "overall_comments": {
                        "type": "string",
                        "maxLength": 2000,
                        "example": "Strong performance across all competencies"
                      },
                      "status": {
                        "type": "string",
                        "enum": [
                          "draft",
                          "in_progress",
                          "completed"
                        ],
                        "example": "completed"
                      },
                      "competency_ratings_attributes": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "required": [
                            "competency_id",
                            "rating_value"
                          ],
                          "properties": {
                            "competency_id": {
                              "type": "integer",
                              "example": 1
                            },
                            "rating_value": {
                              "type": "number",
                              "minimum": 0,
                              "maximum": 5,
                              "example": 4.5
                            },
                            "comments": {
                              "type": "string",
                              "maxLength": 1000,
                              "example": "Excellent communication skills"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Competency assessment created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "competency_assessment": {
                      "type": "object",
                      "description": "Competency assessment with ratings",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 789
                        },
                        "employee": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            }
                          }
                        },
                        "competency_framework": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 678
                            },
                            "name": {
                              "type": "string",
                              "example": "Leadership Competencies"
                            }
                          }
                        },
                        "assessment_type": {
                          "type": "string",
                          "enum": [
                            "self_assessment",
                            "manager_assessment",
                            "peer_assessment",
                            "360_assessment"
                          ],
                          "example": "manager_assessment"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "in_progress",
                            "completed"
                          ],
                          "example": "completed"
                        },
                        "assessed_at": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-15"
                        },
                        "due_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "example": "2026-01-31"
                        },
                        "overall_score": {
                          "type": "number",
                          "nullable": true,
                          "minimum": 0,
                          "maximum": 5,
                          "example": 4.2
                        },
                        "overall_comments": {
                          "type": "string",
                          "nullable": true,
                          "example": "Strong performance across all competencies"
                        },
                        "competency_ratings": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "competency_id": {
                                "type": "integer",
                                "example": 1
                              },
                              "competency_name": {
                                "type": "string",
                                "example": "Communication"
                              },
                              "rating_value": {
                                "type": "number",
                                "minimum": 0,
                                "maximum": 5,
                                "example": 4.5
                              },
                              "comments": {
                                "type": "string",
                                "nullable": true,
                                "example": "Excellent communication skills"
                              }
                            }
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/epms/competency_assessments/{id}": {
      "get": {
        "tags": [
          "EPMS Competency Assessments"
        ],
        "summary": "Get competency assessment details",
        "description": "Retrieves detailed assessment information.\n\n**Required Scopes:** `read:epms_competencies`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Competency assessment ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Competency assessment details retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "competency_assessment": {
                      "type": "object",
                      "description": "Competency assessment with ratings",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 789
                        },
                        "employee": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            }
                          }
                        },
                        "competency_framework": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 678
                            },
                            "name": {
                              "type": "string",
                              "example": "Leadership Competencies"
                            }
                          }
                        },
                        "assessment_type": {
                          "type": "string",
                          "enum": [
                            "self_assessment",
                            "manager_assessment",
                            "peer_assessment",
                            "360_assessment"
                          ],
                          "example": "manager_assessment"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "in_progress",
                            "completed"
                          ],
                          "example": "completed"
                        },
                        "assessed_at": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-15"
                        },
                        "due_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "example": "2026-01-31"
                        },
                        "overall_score": {
                          "type": "number",
                          "nullable": true,
                          "minimum": 0,
                          "maximum": 5,
                          "example": 4.2
                        },
                        "overall_comments": {
                          "type": "string",
                          "nullable": true,
                          "example": "Strong performance across all competencies"
                        },
                        "competency_ratings": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "competency_id": {
                                "type": "integer",
                                "example": 1
                              },
                              "competency_name": {
                                "type": "string",
                                "example": "Communication"
                              },
                              "rating_value": {
                                "type": "number",
                                "minimum": 0,
                                "maximum": 5,
                                "example": 4.5
                              },
                              "comments": {
                                "type": "string",
                                "nullable": true,
                                "example": "Excellent communication skills"
                              }
                            }
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "EPMS Competency Assessments"
        ],
        "summary": "Update competency assessment",
        "description": "Updates an assessment.\n\n**Required Scopes:** `write:epms_competencies`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Competency assessment ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "competency_assessment"
                ],
                "properties": {
                  "competency_assessment": {
                    "type": "object",
                    "description": "Input schema for creating/updating competency assessments",
                    "required": [
                      "employee_id",
                      "competency_framework_id"
                    ],
                    "properties": {
                      "employee_id": {
                        "type": "integer",
                        "example": 456
                      },
                      "competency_framework_id": {
                        "type": "integer",
                        "example": 678
                      },
                      "assessment_type": {
                        "type": "string",
                        "enum": [
                          "self_assessment",
                          "manager_assessment",
                          "peer_assessment",
                          "360_assessment"
                        ],
                        "example": "manager_assessment"
                      },
                      "assessed_at": {
                        "type": "string",
                        "format": "date",
                        "example": "2026-01-15"
                      },
                      "due_date": {
                        "type": "string",
                        "format": "date",
                        "nullable": true,
                        "example": "2026-01-31"
                      },
                      "overall_score": {
                        "type": "number",
                        "minimum": 0,
                        "maximum": 5,
                        "example": 4.2
                      },
                      "overall_comments": {
                        "type": "string",
                        "maxLength": 2000,
                        "example": "Strong performance across all competencies"
                      },
                      "status": {
                        "type": "string",
                        "enum": [
                          "draft",
                          "in_progress",
                          "completed"
                        ],
                        "example": "completed"
                      },
                      "competency_ratings_attributes": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "required": [
                            "competency_id",
                            "rating_value"
                          ],
                          "properties": {
                            "competency_id": {
                              "type": "integer",
                              "example": 1
                            },
                            "rating_value": {
                              "type": "number",
                              "minimum": 0,
                              "maximum": 5,
                              "example": 4.5
                            },
                            "comments": {
                              "type": "string",
                              "maxLength": 1000,
                              "example": "Excellent communication skills"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Competency assessment updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "competency_assessment": {
                      "type": "object",
                      "description": "Competency assessment with ratings",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 789
                        },
                        "employee": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            }
                          }
                        },
                        "competency_framework": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 678
                            },
                            "name": {
                              "type": "string",
                              "example": "Leadership Competencies"
                            }
                          }
                        },
                        "assessment_type": {
                          "type": "string",
                          "enum": [
                            "self_assessment",
                            "manager_assessment",
                            "peer_assessment",
                            "360_assessment"
                          ],
                          "example": "manager_assessment"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "in_progress",
                            "completed"
                          ],
                          "example": "completed"
                        },
                        "assessed_at": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-01-15"
                        },
                        "due_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "example": "2026-01-31"
                        },
                        "overall_score": {
                          "type": "number",
                          "nullable": true,
                          "minimum": 0,
                          "maximum": 5,
                          "example": 4.2
                        },
                        "overall_comments": {
                          "type": "string",
                          "nullable": true,
                          "example": "Strong performance across all competencies"
                        },
                        "competency_ratings": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "competency_id": {
                                "type": "integer",
                                "example": 1
                              },
                              "competency_name": {
                                "type": "string",
                                "example": "Communication"
                              },
                              "rating_value": {
                                "type": "number",
                                "minimum": 0,
                                "maximum": 5,
                                "example": 4.5
                              },
                              "comments": {
                                "type": "string",
                                "nullable": true,
                                "example": "Excellent communication skills"
                              }
                            }
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-01-15T14:30:00Z"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/leave_requests": {
      "get": {
        "tags": [
          "Leave Management"
        ],
        "summary": "List user's leave requests",
        "description": "Get a paginated list of leave requests for the authenticated user.\nSupports filtering by status, leave type, and date ranges.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "description": "Number of items per page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by status (comma-separated for multiple)",
            "schema": {
              "type": "string",
              "example": "pending,approved"
            }
          },
          {
            "name": "leave_type_id",
            "in": "query",
            "description": "Filter by leave type ID",
            "schema": {
              "type": "integer",
              "example": 1
            }
          },
          {
            "name": "start_date",
            "in": "query",
            "description": "Filter requests starting from this date",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2024-01-01"
            }
          },
          {
            "name": "end_date",
            "in": "query",
            "description": "Filter requests ending before this date",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2024-12-31"
            }
          },
          {
            "name": "period",
            "in": "query",
            "description": "Filter by time period",
            "schema": {
              "type": "string",
              "enum": [
                "upcoming",
                "past",
                "current"
              ],
              "example": "upcoming"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Leave requests retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "Basic leave request information",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "description": "Unique leave request ID",
                            "example": 123
                          },
                          "user_id": {
                            "type": "integer",
                            "description": "ID of the user who created the request",
                            "example": 456
                          },
                          "leave_type": {
                            "$ref": "#/components/schemas/LeaveTypeBasic"
                          },
                          "start_date": {
                            "type": "string",
                            "format": "date",
                            "description": "Start date of leave",
                            "example": "2024-03-15"
                          },
                          "end_date": {
                            "type": "string",
                            "format": "date",
                            "description": "End date of leave",
                            "example": "2024-03-17"
                          },
                          "hours_calculated": {
                            "type": "number",
                            "description": "Total hours for this leave request (primary field)",
                            "example": 24
                          },
                          "business_days": {
                            "type": "number",
                            "description": "DEPRECATED: Use hours_calculated instead. Returns same value as hours_calculated for backward compatibility.",
                            "example": 24
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "pending",
                              "approved",
                              "denied",
                              "cancelled",
                              "special_approval"
                            ],
                            "description": "Current status of the leave request",
                            "example": "pending"
                          },
                          "notes": {
                            "type": "string",
                            "nullable": true,
                            "description": "Optional notes for the leave request",
                            "example": "Family vacation"
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When the request was created",
                            "example": "2024-02-15T10:00:00Z"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When the request was last updated",
                            "example": "2024-02-15T10:00:00Z"
                          }
                        },
                        "required": [
                          "id",
                          "user_id",
                          "leave_type",
                          "start_date",
                          "end_date",
                          "hours_calculated",
                          "status",
                          "created_at",
                          "updated_at"
                        ]
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 6
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Items per page",
                          "example": 25
                        }
                      },
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Leave Management"
        ],
        "summary": "Create a new leave request",
        "description": "Create a new leave request for the authenticated user.\nThe request will be validated against business rules, blackout periods, and coverage limits.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "leave_type_id",
                  "start_date",
                  "end_date"
                ],
                "properties": {
                  "leave_type_id": {
                    "type": "integer",
                    "description": "ID of the leave type",
                    "example": 1
                  },
                  "start_date": {
                    "type": "string",
                    "format": "date",
                    "description": "Start date of leave",
                    "example": "2024-03-15"
                  },
                  "end_date": {
                    "type": "string",
                    "format": "date",
                    "description": "End date of leave",
                    "example": "2024-03-17"
                  },
                  "notes": {
                    "type": "string",
                    "description": "Optional notes for the leave request",
                    "example": "Family vacation"
                  },
                  "half_day": {
                    "type": "boolean",
                    "description": "Whether this is a half-day request",
                    "example": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Leave request created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "leave_request": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LeaveRequest"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "approval_details": {
                              "type": "object",
                              "description": "Approval workflow information",
                              "properties": {
                                "requires_approval": {
                                  "type": "boolean",
                                  "description": "Whether this request requires approval",
                                  "example": true
                                },
                                "approved_by": {
                                  "type": "object",
                                  "nullable": true,
                                  "description": "User who approved the request",
                                  "properties": {
                                    "id": {
                                      "type": "integer",
                                      "example": 789
                                    },
                                    "name": {
                                      "type": "string",
                                      "example": "Jane Manager"
                                    }
                                  }
                                },
                                "approved_at": {
                                  "type": "string",
                                  "format": "date-time",
                                  "nullable": true,
                                  "description": "When the request was approved",
                                  "example": "2024-02-16T14:30:00Z"
                                },
                                "denial_reason": {
                                  "type": "string",
                                  "nullable": true,
                                  "description": "Reason for denial if applicable",
                                  "example": "Insufficient coverage during requested period"
                                }
                              }
                            },
                            "conflicts": {
                              "type": "object",
                              "description": "Shift conflict information",
                              "properties": {
                                "has_conflicts": {
                                  "type": "boolean",
                                  "description": "Whether this request conflicts with assigned shifts",
                                  "example": false
                                },
                                "conflicting_shifts": {
                                  "type": "array",
                                  "description": "List of conflicting shifts",
                                  "items": {
                                    "$ref": "#/components/schemas/ShiftConflict"
                                  }
                                }
                              }
                            },
                            "blackout_warnings": {
                              "type": "array",
                              "description": "Blackout period warnings",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 1
                                  },
                                  "name": {
                                    "type": "string",
                                    "example": "Winter Holiday Season"
                                  },
                                  "start_date": {
                                    "type": "string",
                                    "format": "date",
                                    "example": "2024-12-15"
                                  },
                                  "end_date": {
                                    "type": "string",
                                    "format": "date",
                                    "example": "2024-12-31"
                                  },
                                  "blocks_requests": {
                                    "type": "boolean",
                                    "example": true
                                  },
                                  "message": {
                                    "type": "string",
                                    "example": "Leave requests are blocked during Winter Holiday Season"
                                  }
                                }
                              }
                            },
                            "coverage_impact": {
                              "type": "object",
                              "nullable": true,
                              "description": "Coverage impact analysis",
                              "properties": {
                                "affects_coverage": {
                                  "type": "boolean",
                                  "example": false
                                },
                                "coverage_percentage": {
                                  "type": "number",
                                  "example": 85.5
                                },
                                "available_spots": {
                                  "type": "integer",
                                  "example": 3
                                },
                                "message": {
                                  "type": "string",
                                  "example": "This request will not affect coverage limits"
                                }
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          }
        }
      }
    },
    "/leave_requests/{id}": {
      "get": {
        "tags": [
          "Leave Management"
        ],
        "summary": "Get a specific leave request",
        "description": "Get detailed information about a specific leave request",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Leave request ID",
            "schema": {
              "type": "integer",
              "example": 123
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Leave request retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "leave_request": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LeaveRequest"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "approval_details": {
                              "type": "object",
                              "description": "Approval workflow information",
                              "properties": {
                                "requires_approval": {
                                  "type": "boolean",
                                  "description": "Whether this request requires approval",
                                  "example": true
                                },
                                "approved_by": {
                                  "type": "object",
                                  "nullable": true,
                                  "description": "User who approved the request",
                                  "properties": {
                                    "id": {
                                      "type": "integer",
                                      "example": 789
                                    },
                                    "name": {
                                      "type": "string",
                                      "example": "Jane Manager"
                                    }
                                  }
                                },
                                "approved_at": {
                                  "type": "string",
                                  "format": "date-time",
                                  "nullable": true,
                                  "description": "When the request was approved",
                                  "example": "2024-02-16T14:30:00Z"
                                },
                                "denial_reason": {
                                  "type": "string",
                                  "nullable": true,
                                  "description": "Reason for denial if applicable",
                                  "example": "Insufficient coverage during requested period"
                                }
                              }
                            },
                            "conflicts": {
                              "type": "object",
                              "description": "Shift conflict information",
                              "properties": {
                                "has_conflicts": {
                                  "type": "boolean",
                                  "description": "Whether this request conflicts with assigned shifts",
                                  "example": false
                                },
                                "conflicting_shifts": {
                                  "type": "array",
                                  "description": "List of conflicting shifts",
                                  "items": {
                                    "$ref": "#/components/schemas/ShiftConflict"
                                  }
                                }
                              }
                            },
                            "blackout_warnings": {
                              "type": "array",
                              "description": "Blackout period warnings",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 1
                                  },
                                  "name": {
                                    "type": "string",
                                    "example": "Winter Holiday Season"
                                  },
                                  "start_date": {
                                    "type": "string",
                                    "format": "date",
                                    "example": "2024-12-15"
                                  },
                                  "end_date": {
                                    "type": "string",
                                    "format": "date",
                                    "example": "2024-12-31"
                                  },
                                  "blocks_requests": {
                                    "type": "boolean",
                                    "example": true
                                  },
                                  "message": {
                                    "type": "string",
                                    "example": "Leave requests are blocked during Winter Holiday Season"
                                  }
                                }
                              }
                            },
                            "coverage_impact": {
                              "type": "object",
                              "nullable": true,
                              "description": "Coverage impact analysis",
                              "properties": {
                                "affects_coverage": {
                                  "type": "boolean",
                                  "example": false
                                },
                                "coverage_percentage": {
                                  "type": "number",
                                  "example": 85.5
                                },
                                "available_spots": {
                                  "type": "integer",
                                  "example": 3
                                },
                                "message": {
                                  "type": "string",
                                  "example": "This request will not affect coverage limits"
                                }
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Leave Management"
        ],
        "summary": "Update a leave request",
        "description": "Update a pending leave request. Only pending requests can be modified.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Leave request ID",
            "schema": {
              "type": "integer",
              "example": 123
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "leave_type_id": {
                    "type": "integer",
                    "description": "ID of the leave type",
                    "example": 1
                  },
                  "start_date": {
                    "type": "string",
                    "format": "date",
                    "description": "Start date of leave",
                    "example": "2024-03-15"
                  },
                  "end_date": {
                    "type": "string",
                    "format": "date",
                    "description": "End date of leave",
                    "example": "2024-03-17"
                  },
                  "notes": {
                    "type": "string",
                    "description": "Optional notes for the leave request",
                    "example": "Updated vacation dates"
                  },
                  "half_day": {
                    "type": "boolean",
                    "description": "Whether this is a half-day request",
                    "example": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Leave request updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "leave_request": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LeaveRequest"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "approval_details": {
                              "type": "object",
                              "description": "Approval workflow information",
                              "properties": {
                                "requires_approval": {
                                  "type": "boolean",
                                  "description": "Whether this request requires approval",
                                  "example": true
                                },
                                "approved_by": {
                                  "type": "object",
                                  "nullable": true,
                                  "description": "User who approved the request",
                                  "properties": {
                                    "id": {
                                      "type": "integer",
                                      "example": 789
                                    },
                                    "name": {
                                      "type": "string",
                                      "example": "Jane Manager"
                                    }
                                  }
                                },
                                "approved_at": {
                                  "type": "string",
                                  "format": "date-time",
                                  "nullable": true,
                                  "description": "When the request was approved",
                                  "example": "2024-02-16T14:30:00Z"
                                },
                                "denial_reason": {
                                  "type": "string",
                                  "nullable": true,
                                  "description": "Reason for denial if applicable",
                                  "example": "Insufficient coverage during requested period"
                                }
                              }
                            },
                            "conflicts": {
                              "type": "object",
                              "description": "Shift conflict information",
                              "properties": {
                                "has_conflicts": {
                                  "type": "boolean",
                                  "description": "Whether this request conflicts with assigned shifts",
                                  "example": false
                                },
                                "conflicting_shifts": {
                                  "type": "array",
                                  "description": "List of conflicting shifts",
                                  "items": {
                                    "$ref": "#/components/schemas/ShiftConflict"
                                  }
                                }
                              }
                            },
                            "blackout_warnings": {
                              "type": "array",
                              "description": "Blackout period warnings",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 1
                                  },
                                  "name": {
                                    "type": "string",
                                    "example": "Winter Holiday Season"
                                  },
                                  "start_date": {
                                    "type": "string",
                                    "format": "date",
                                    "example": "2024-12-15"
                                  },
                                  "end_date": {
                                    "type": "string",
                                    "format": "date",
                                    "example": "2024-12-31"
                                  },
                                  "blocks_requests": {
                                    "type": "boolean",
                                    "example": true
                                  },
                                  "message": {
                                    "type": "string",
                                    "example": "Leave requests are blocked during Winter Holiday Season"
                                  }
                                }
                              }
                            },
                            "coverage_impact": {
                              "type": "object",
                              "nullable": true,
                              "description": "Coverage impact analysis",
                              "properties": {
                                "affects_coverage": {
                                  "type": "boolean",
                                  "example": false
                                },
                                "coverage_percentage": {
                                  "type": "number",
                                  "example": 85.5
                                },
                                "available_spots": {
                                  "type": "integer",
                                  "example": 3
                                },
                                "message": {
                                  "type": "string",
                                  "example": "This request will not affect coverage limits"
                                }
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Leave request cannot be modified"
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Leave Management"
        ],
        "summary": "Delete a leave request",
        "description": "Delete a leave request. Only pending requests can be deleted.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Leave request ID",
            "schema": {
              "type": "integer",
              "example": 123
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Leave request deleted successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Leave request deleted successfully"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Leave request cannot be deleted"
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/leave_requests/{id}/cancel": {
      "post": {
        "tags": [
          "Leave Management"
        ],
        "summary": "Cancel a leave request",
        "description": "Cancel a leave request. This action is different from delete and may restore leave balances.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Leave request ID",
            "schema": {
              "type": "integer",
              "example": 123
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Leave request cancelled successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "leave_request": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LeaveRequest"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "approval_details": {
                              "type": "object",
                              "description": "Approval workflow information",
                              "properties": {
                                "requires_approval": {
                                  "type": "boolean",
                                  "description": "Whether this request requires approval",
                                  "example": true
                                },
                                "approved_by": {
                                  "type": "object",
                                  "nullable": true,
                                  "description": "User who approved the request",
                                  "properties": {
                                    "id": {
                                      "type": "integer",
                                      "example": 789
                                    },
                                    "name": {
                                      "type": "string",
                                      "example": "Jane Manager"
                                    }
                                  }
                                },
                                "approved_at": {
                                  "type": "string",
                                  "format": "date-time",
                                  "nullable": true,
                                  "description": "When the request was approved",
                                  "example": "2024-02-16T14:30:00Z"
                                },
                                "denial_reason": {
                                  "type": "string",
                                  "nullable": true,
                                  "description": "Reason for denial if applicable",
                                  "example": "Insufficient coverage during requested period"
                                }
                              }
                            },
                            "conflicts": {
                              "type": "object",
                              "description": "Shift conflict information",
                              "properties": {
                                "has_conflicts": {
                                  "type": "boolean",
                                  "description": "Whether this request conflicts with assigned shifts",
                                  "example": false
                                },
                                "conflicting_shifts": {
                                  "type": "array",
                                  "description": "List of conflicting shifts",
                                  "items": {
                                    "$ref": "#/components/schemas/ShiftConflict"
                                  }
                                }
                              }
                            },
                            "blackout_warnings": {
                              "type": "array",
                              "description": "Blackout period warnings",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 1
                                  },
                                  "name": {
                                    "type": "string",
                                    "example": "Winter Holiday Season"
                                  },
                                  "start_date": {
                                    "type": "string",
                                    "format": "date",
                                    "example": "2024-12-15"
                                  },
                                  "end_date": {
                                    "type": "string",
                                    "format": "date",
                                    "example": "2024-12-31"
                                  },
                                  "blocks_requests": {
                                    "type": "boolean",
                                    "example": true
                                  },
                                  "message": {
                                    "type": "string",
                                    "example": "Leave requests are blocked during Winter Holiday Season"
                                  }
                                }
                              }
                            },
                            "coverage_impact": {
                              "type": "object",
                              "nullable": true,
                              "description": "Coverage impact analysis",
                              "properties": {
                                "affects_coverage": {
                                  "type": "boolean",
                                  "example": false
                                },
                                "coverage_percentage": {
                                  "type": "number",
                                  "example": 85.5
                                },
                                "available_spots": {
                                  "type": "integer",
                                  "example": 3
                                },
                                "message": {
                                  "type": "string",
                                  "example": "This request will not affect coverage limits"
                                }
                              }
                            }
                          }
                        }
                      ]
                    },
                    "message": {
                      "type": "string",
                      "example": "Leave request cancelled successfully"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/leave_requests/conflicts": {
      "get": {
        "tags": [
          "Leave Management"
        ],
        "summary": "Check for shift conflicts",
        "description": "Check if a proposed leave request would conflict with assigned shifts.\nThis is typically used before creating a leave request to warn users.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "start_date",
            "in": "query",
            "required": true,
            "description": "Proposed start date",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2024-03-15"
            }
          },
          {
            "name": "end_date",
            "in": "query",
            "required": true,
            "description": "Proposed end date",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2024-03-17"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Conflict check completed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conflicts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "Information about a conflicting shift",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "description": "Shift ID",
                            "example": 789
                          },
                          "name": {
                            "type": "string",
                            "description": "Shift name",
                            "example": "Morning Shift"
                          },
                          "start_time": {
                            "type": "string",
                            "format": "date-time",
                            "description": "Shift start time",
                            "example": "2024-03-15T08:00:00Z"
                          },
                          "end_time": {
                            "type": "string",
                            "format": "date-time",
                            "description": "Shift end time",
                            "example": "2024-03-15T16:00:00Z"
                          },
                          "location": {
                            "type": "object",
                            "nullable": true,
                            "description": "Shift location",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 1
                              },
                              "name": {
                                "type": "string",
                                "example": "Main Office"
                              }
                            }
                          }
                        },
                        "required": [
                          "id",
                          "name",
                          "start_time",
                          "end_time"
                        ]
                      }
                    },
                    "has_conflicts": {
                      "type": "boolean",
                      "example": false
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/leave_requests/history": {
      "get": {
        "tags": [
          "Leave Management"
        ],
        "summary": "Get leave request history with status changes",
        "description": "Get detailed history of leave requests including all status changes and approval workflow.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "description": "Number of items per page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "start_date",
            "in": "query",
            "description": "Filter history from this date (default: 12 months ago)",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2023-03-01"
            }
          },
          {
            "name": "end_date",
            "in": "query",
            "description": "Filter history until this date (default: today)",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2024-03-01"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Leave request history retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/LeaveRequestDetailed"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "status_history": {
                                "type": "array",
                                "description": "Complete status change history",
                                "items": {
                                  "type": "object",
                                  "properties": {
                                    "id": {
                                      "type": "integer",
                                      "example": 1
                                    },
                                    "from_status": {
                                      "type": "string",
                                      "example": "pending"
                                    },
                                    "to_status": {
                                      "type": "string",
                                      "example": "approved"
                                    },
                                    "changed_by": {
                                      "type": "object",
                                      "nullable": true,
                                      "properties": {
                                        "id": {
                                          "type": "integer",
                                          "example": 2
                                        },
                                        "name": {
                                          "type": "string",
                                          "example": "Manager Name"
                                        }
                                      }
                                    },
                                    "notes": {
                                      "type": "string",
                                      "nullable": true,
                                      "example": "Approved for vacation"
                                    },
                                    "changed_at": {
                                      "type": "string",
                                      "format": "date-time",
                                      "example": "2024-02-16T14:30:00Z"
                                    }
                                  }
                                }
                              }
                            }
                          }
                        ]
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 6
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Items per page",
                          "example": 25
                        }
                      },
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/leave_requests/analytics": {
      "get": {
        "tags": [
          "Leave Management"
        ],
        "summary": "Get leave usage analytics",
        "description": "Get comprehensive analytics about leave usage patterns, trends, and statistics.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "start_date",
            "in": "query",
            "description": "Analytics period start date (default: beginning of year)",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2024-01-01"
            }
          },
          {
            "name": "end_date",
            "in": "query",
            "description": "Analytics period end date (default: end of year)",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2024-12-31"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Leave analytics retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Comprehensive leave usage analytics",
                  "properties": {
                    "period": {
                      "type": "object",
                      "description": "Analytics period",
                      "properties": {
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-01-01"
                        },
                        "end_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-12-31"
                        },
                        "total_days": {
                          "type": "integer",
                          "example": 366
                        }
                      }
                    },
                    "summary": {
                      "type": "object",
                      "description": "Overall summary statistics",
                      "properties": {
                        "total_requests": {
                          "type": "integer",
                          "example": 25
                        },
                        "total_leave_days": {
                          "type": "integer",
                          "example": 75
                        },
                        "approved_requests": {
                          "type": "integer",
                          "example": 20
                        },
                        "approved_days": {
                          "type": "integer",
                          "example": 60
                        },
                        "pending_requests": {
                          "type": "integer",
                          "example": 3
                        },
                        "denied_requests": {
                          "type": "integer",
                          "example": 1
                        },
                        "cancelled_requests": {
                          "type": "integer",
                          "example": 1
                        }
                      }
                    },
                    "by_leave_type": {
                      "type": "array",
                      "description": "Analytics grouped by leave type",
                      "items": {
                        "type": "object",
                        "properties": {
                          "leave_type": {
                            "$ref": "#/components/schemas/LeaveTypeBasic"
                          },
                          "total_requests": {
                            "type": "integer",
                            "example": 15
                          },
                          "total_days": {
                            "type": "integer",
                            "example": 45
                          },
                          "approved_requests": {
                            "type": "integer",
                            "example": 12
                          },
                          "approved_days": {
                            "type": "integer",
                            "example": 36
                          },
                          "pending_requests": {
                            "type": "integer",
                            "example": 2
                          },
                          "denied_requests": {
                            "type": "integer",
                            "example": 1
                          },
                          "cancelled_requests": {
                            "type": "integer",
                            "example": 0
                          }
                        }
                      }
                    },
                    "by_month": {
                      "type": "array",
                      "description": "Analytics grouped by month",
                      "items": {
                        "type": "object",
                        "properties": {
                          "month": {
                            "type": "string",
                            "example": "2024-03"
                          },
                          "month_name": {
                            "type": "string",
                            "example": "March 2024"
                          },
                          "total_requests": {
                            "type": "integer",
                            "example": 5
                          },
                          "total_days": {
                            "type": "integer",
                            "example": 15
                          },
                          "approved_days": {
                            "type": "integer",
                            "example": 12
                          }
                        }
                      }
                    },
                    "by_status": {
                      "type": "array",
                      "description": "Analytics grouped by status",
                      "items": {
                        "type": "object",
                        "properties": {
                          "status": {
                            "type": "string",
                            "example": "approved"
                          },
                          "count": {
                            "type": "integer",
                            "example": 20
                          },
                          "total_days": {
                            "type": "integer",
                            "example": 60
                          },
                          "percentage": {
                            "type": "number",
                            "example": 80.0
                          }
                        }
                      }
                    },
                    "usage_patterns": {
                      "type": "object",
                      "description": "Usage patterns and insights",
                      "properties": {
                        "most_used_leave_type": {
                          "type": "object",
                          "nullable": true,
                          "description": "Leave type with most usage"
                        },
                        "busiest_month": {
                          "type": "object",
                          "nullable": true,
                          "description": "Month with most leave days"
                        },
                        "average_request_length": {
                          "type": "number",
                          "description": "Average length of leave requests in days",
                          "example": 3.0
                        },
                        "longest_request": {
                          "type": "object",
                          "nullable": true,
                          "description": "Longest leave request"
                        },
                        "approval_rate": {
                          "type": "number",
                          "description": "Percentage of requests approved",
                          "example": 80.0
                        }
                      }
                    }
                  },
                  "required": [
                    "period",
                    "summary",
                    "by_leave_type",
                    "by_month",
                    "by_status",
                    "usage_patterns"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/leave_requests/bulk": {
      "post": {
        "tags": [
          "Leave Management"
        ],
        "summary": "Create multiple leave requests",
        "description": "Create multiple leave requests in a single operation. Maximum 10 requests per batch.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "leave_requests"
                ],
                "properties": {
                  "leave_requests": {
                    "type": "array",
                    "maxItems": 10,
                    "items": {
                      "type": "object",
                      "required": [
                        "leave_type_id",
                        "start_date",
                        "end_date"
                      ],
                      "properties": {
                        "leave_type_id": {
                          "type": "integer",
                          "example": 1
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-03-15"
                        },
                        "end_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-03-17"
                        },
                        "notes": {
                          "type": "string",
                          "example": "Vacation"
                        },
                        "half_day": {
                          "type": "boolean",
                          "example": false
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Bulk leave requests processed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Response from bulk leave request operation",
                  "properties": {
                    "results": {
                      "type": "array",
                      "description": "Successful requests",
                      "items": {
                        "type": "object",
                        "properties": {
                          "index": {
                            "type": "integer",
                            "description": "Index of request in original array",
                            "example": 0
                          },
                          "success": {
                            "type": "boolean",
                            "example": true
                          },
                          "leave_request": {
                            "$ref": "#/components/schemas/LeaveRequestDetailed"
                          }
                        }
                      }
                    },
                    "errors": {
                      "type": "array",
                      "description": "Failed requests",
                      "items": {
                        "type": "object",
                        "properties": {
                          "index": {
                            "type": "integer",
                            "description": "Index of request in original array",
                            "example": 1
                          },
                          "success": {
                            "type": "boolean",
                            "example": false
                          },
                          "errors": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            },
                            "example": [
                              "Start date cannot be in the past"
                            ]
                          }
                        }
                      }
                    },
                    "summary": {
                      "type": "object",
                      "description": "Summary of bulk operation",
                      "properties": {
                        "total_requests": {
                          "type": "integer",
                          "example": 5
                        },
                        "successful": {
                          "type": "integer",
                          "example": 4
                        },
                        "failed": {
                          "type": "integer",
                          "example": 1
                        }
                      }
                    }
                  },
                  "required": [
                    "results",
                    "errors",
                    "summary"
                  ]
                }
              }
            }
          },
          "422": {
            "description": "Some or all requests failed validation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Response from bulk leave request operation",
                  "properties": {
                    "results": {
                      "type": "array",
                      "description": "Successful requests",
                      "items": {
                        "type": "object",
                        "properties": {
                          "index": {
                            "type": "integer",
                            "description": "Index of request in original array",
                            "example": 0
                          },
                          "success": {
                            "type": "boolean",
                            "example": true
                          },
                          "leave_request": {
                            "$ref": "#/components/schemas/LeaveRequestDetailed"
                          }
                        }
                      }
                    },
                    "errors": {
                      "type": "array",
                      "description": "Failed requests",
                      "items": {
                        "type": "object",
                        "properties": {
                          "index": {
                            "type": "integer",
                            "description": "Index of request in original array",
                            "example": 1
                          },
                          "success": {
                            "type": "boolean",
                            "example": false
                          },
                          "errors": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            },
                            "example": [
                              "Start date cannot be in the past"
                            ]
                          }
                        }
                      }
                    },
                    "summary": {
                      "type": "object",
                      "description": "Summary of bulk operation",
                      "properties": {
                        "total_requests": {
                          "type": "integer",
                          "example": 5
                        },
                        "successful": {
                          "type": "integer",
                          "example": 4
                        },
                        "failed": {
                          "type": "integer",
                          "example": 1
                        }
                      }
                    }
                  },
                  "required": [
                    "results",
                    "errors",
                    "summary"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/leave_balances": {
      "get": {
        "tags": [
          "Leave Management"
        ],
        "summary": "Get user's leave balances",
        "description": "Get leave balances for all leave types available to the user.\nIncludes accrued, used, available amounts and usage percentages.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Leave balances retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "Leave balance information for a specific leave type. All numeric values are in HOURS.",
                        "properties": {
                          "leave_type_id": {
                            "type": "integer",
                            "description": "ID of the leave type",
                            "example": 1
                          },
                          "leave_type": {
                            "$ref": "#/components/schemas/LeaveTypeBasic"
                          },
                          "year": {
                            "type": "integer",
                            "description": "Year for this balance",
                            "example": 2024
                          },
                          "unit": {
                            "type": "string",
                            "description": "Unit of measurement for balance values",
                            "example": "hours",
                            "enum": [
                              "hours"
                            ]
                          },
                          "accrued": {
                            "type": "number",
                            "description": "Total hours accrued for the year",
                            "example": 120.0
                          },
                          "used": {
                            "type": "number",
                            "description": "Hours already used",
                            "example": 40.0
                          },
                          "balance": {
                            "type": "number",
                            "description": "Available hours remaining",
                            "example": 80.0
                          },
                          "pending": {
                            "type": "number",
                            "description": "Hours in pending requests",
                            "example": 24.0
                          },
                          "percentages": {
                            "type": "object",
                            "description": "Usage percentages for progress indicators",
                            "properties": {
                              "used": {
                                "type": "number",
                                "description": "Percentage of accrued hours used",
                                "example": 33.3
                              },
                              "pending": {
                                "type": "number",
                                "description": "Percentage of accrued hours pending",
                                "example": 20.0
                              },
                              "available": {
                                "type": "number",
                                "description": "Percentage of accrued hours available",
                                "example": 46.7
                              }
                            }
                          }
                        },
                        "required": [
                          "leave_type_id",
                          "leave_type",
                          "year",
                          "accrued",
                          "used",
                          "balance",
                          "pending",
                          "percentages"
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/leave_balances/{leave_type_id}": {
      "get": {
        "tags": [
          "Leave Management"
        ],
        "summary": "Get balance for specific leave type",
        "description": "Get detailed balance information for a specific leave type",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "leave_type_id",
            "in": "path",
            "required": true,
            "description": "Leave type ID",
            "schema": {
              "type": "integer",
              "example": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Leave balance retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "leave_balance": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LeaveBalance"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "recent_usage": {
                              "type": "array",
                              "description": "Leave consumed against this balance in the last 6 months, newest first. Includes approved leave requests, usage an administrator recorded by hand, and hours paid out from banked time — all three move the balance's `used` figure. Pending requests are NOT included (they are not usage yet); read the balance's `pending` hours for those.",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "description": "Identifier within the table named by `record_type`.",
                                    "example": 123
                                  },
                                  "record_type": {
                                    "type": "string",
                                    "description": "Which record `id` refers to. `leave_request` for an approved request; `leave_ledger_entry` for administrator- recorded usage or a banked-time payout.",
                                    "enum": [
                                      "leave_request",
                                      "leave_ledger_entry"
                                    ],
                                    "example": "leave_request"
                                  },
                                  "source": {
                                    "type": "string",
                                    "enum": [
                                      "leave_request",
                                      "manual",
                                      "payout"
                                    ],
                                    "example": "leave_request"
                                  },
                                  "start_date": {
                                    "type": "string",
                                    "format": "date",
                                    "example": "2024-01-15"
                                  },
                                  "end_date": {
                                    "type": "string",
                                    "format": "date",
                                    "example": "2024-01-17"
                                  },
                                  "hours": {
                                    "type": "number",
                                    "description": "Hours actually charged to the balance (the paid portion). Use this figure to reconcile against `used`.",
                                    "example": 16
                                  },
                                  "unpaid_hours": {
                                    "type": "number",
                                    "description": "Portion taken as unpaid leave, which never touches the balance.",
                                    "example": 8
                                  },
                                  "hours_calculated": {
                                    "type": "number",
                                    "description": "Total hours on the request, including any unpaid portion. Equals `hours` unless the request has an unpaid split.",
                                    "example": 24
                                  },
                                  "business_days": {
                                    "type": "number",
                                    "description": "DEPRECATED: Use hours_calculated",
                                    "example": 24
                                  },
                                  "status": {
                                    "type": "string",
                                    "nullable": true,
                                    "description": "Always `approved` for a leave request; null for ledger entries.",
                                    "example": "approved"
                                  },
                                  "upcoming": {
                                    "type": "boolean",
                                    "description": "Approved but the time off has not happened yet.",
                                    "example": false
                                  },
                                  "counts_toward_balance": {
                                    "type": "boolean",
                                    "description": "False for a historical record imported alongside the balance figures its hours are already inside. Such rows are listed but excluded from `used`.",
                                    "example": true
                                  },
                                  "notes": {
                                    "type": "string",
                                    "nullable": true,
                                    "example": "Vacation"
                                  },
                                  "edited": {
                                    "type": "boolean",
                                    "description": "The leave request has been modified since it was submitted.",
                                    "example": false
                                  },
                                  "edit_count": {
                                    "type": "integer",
                                    "example": 0
                                  },
                                  "last_edited_at": {
                                    "type": "string",
                                    "format": "date-time",
                                    "nullable": true,
                                    "example": "2024-02-01T09:30:00Z"
                                  }
                                }
                              }
                            },
                            "accrual_policy": {
                              "type": "object",
                              "nullable": true,
                              "description": "Accrual policy information",
                              "properties": {
                                "id": {
                                  "type": "integer",
                                  "example": 1
                                },
                                "name": {
                                  "type": "string",
                                  "example": "Standard Vacation Policy"
                                },
                                "accrual_rate": {
                                  "type": "number",
                                  "description": "Hours accrued per period",
                                  "example": 3.08
                                },
                                "accrual_unit": {
                                  "type": "string",
                                  "description": "Unit for all accrual values",
                                  "example": "hours",
                                  "enum": [
                                    "hours"
                                  ]
                                },
                                "accrual_frequency": {
                                  "type": "string",
                                  "example": "bi-weekly"
                                },
                                "max_balance": {
                                  "type": "number",
                                  "description": "Maximum balance cap in hours",
                                  "nullable": true,
                                  "example": 120.0
                                },
                                "max_accrual": {
                                  "type": "number",
                                  "description": "Deprecated, use max_balance",
                                  "nullable": true,
                                  "example": 120.0
                                },
                                "carryover_allowed": {
                                  "type": "boolean",
                                  "example": true
                                },
                                "carryover_max": {
                                  "type": "number",
                                  "description": "Maximum hours to carry over",
                                  "nullable": true,
                                  "example": 40.0
                                },
                                "waiting_period_days": {
                                  "type": "integer",
                                  "description": "Calendar days before accrual starts",
                                  "example": 90
                                },
                                "proration_enabled": {
                                  "type": "boolean",
                                  "example": true
                                }
                              }
                            },
                            "projected_balance": {
                              "type": "number",
                              "description": "Projected balance (hours) at end of year",
                              "example": 100.0
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/leave_balances/summary": {
      "get": {
        "tags": [
          "Leave Management"
        ],
        "summary": "Get leave balance summary",
        "description": "Get a comprehensive summary of all leave balances including totals,\nupcoming leave, and usage analytics.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Leave balance summary retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Comprehensive leave balance summary",
                  "properties": {
                    "totals": {
                      "type": "object",
                      "description": "Totals across all leave types",
                      "properties": {
                        "accrued": {
                          "type": "number",
                          "example": 25.0
                        },
                        "used": {
                          "type": "number",
                          "example": 8.0
                        },
                        "pending": {
                          "type": "number",
                          "example": 3.0
                        },
                        "available": {
                          "type": "number",
                          "example": 14.0
                        },
                        "usage_percentage": {
                          "type": "number",
                          "example": 32.0
                        }
                      }
                    },
                    "by_leave_type": {
                      "type": "array",
                      "description": "Balances by leave type",
                      "items": {
                        "$ref": "#/components/schemas/LeaveBalance"
                      }
                    },
                    "upcoming_leave": {
                      "type": "array",
                      "description": "Upcoming approved leave",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 123
                          },
                          "leave_type": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 1
                              },
                              "name": {
                                "type": "string",
                                "example": "Vacation"
                              },
                              "color": {
                                "type": "string",
                                "example": "#4CAF50"
                              }
                            }
                          },
                          "start_date": {
                            "type": "string",
                            "format": "date",
                            "example": "2024-04-15"
                          },
                          "end_date": {
                            "type": "string",
                            "format": "date",
                            "example": "2024-04-17"
                          },
                          "hours": {
                            "type": "number",
                            "description": "Hours that will be charged to the balance (the paid portion).",
                            "example": 16
                          },
                          "unpaid_hours": {
                            "type": "number",
                            "description": "Portion to be taken as unpaid leave, which never touches the balance.",
                            "example": 8
                          },
                          "hours_calculated": {
                            "type": "number",
                            "description": "Total hours on the request, including any unpaid portion. Equals `hours` unless the request has an unpaid split.",
                            "example": 24
                          },
                          "business_days": {
                            "type": "number",
                            "description": "DEPRECATED: Use hours_calculated",
                            "example": 24
                          },
                          "notes": {
                            "type": "string",
                            "nullable": true,
                            "example": "Spring vacation"
                          }
                        }
                      }
                    },
                    "year": {
                      "type": "integer",
                      "description": "Year for this summary",
                      "example": 2024
                    }
                  },
                  "required": [
                    "totals",
                    "by_leave_type",
                    "upcoming_leave",
                    "year"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/leave_types": {
      "get": {
        "tags": [
          "Leave Management"
        ],
        "summary": "List available leave types",
        "description": "Get all active leave types available to the user's business.\nOptionally include current balance information.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "include_balances",
            "in": "query",
            "description": "Include current balance information",
            "schema": {
              "type": "boolean",
              "default": false,
              "example": true
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Leave types retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "Leave type information",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "description": "Unique leave type ID",
                            "example": 1
                          },
                          "name": {
                            "type": "string",
                            "description": "Name of the leave type",
                            "example": "Vacation"
                          },
                          "color": {
                            "type": "string",
                            "description": "Color code for UI display",
                            "example": "#4CAF50"
                          },
                          "icon": {
                            "type": "string",
                            "description": "Icon class for UI display",
                            "example": "fas fa-calendar-star"
                          },
                          "active": {
                            "type": "boolean",
                            "description": "Whether this leave type is active",
                            "example": true
                          },
                          "default": {
                            "type": "boolean",
                            "description": "Whether this is the default leave type",
                            "example": false
                          },
                          "current_balance": {
                            "type": "object",
                            "nullable": true,
                            "description": "Current balance for this leave type (if requested)",
                            "properties": {
                              "accrued": {
                                "type": "number",
                                "example": 15.0
                              },
                              "used": {
                                "type": "number",
                                "example": 5.0
                              },
                              "balance": {
                                "type": "number",
                                "example": 10.0
                              },
                              "year": {
                                "type": "integer",
                                "example": 2024
                              }
                            }
                          }
                        },
                        "required": [
                          "id",
                          "name",
                          "color",
                          "icon",
                          "active",
                          "default"
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/leave_types/{id}": {
      "get": {
        "tags": [
          "Leave Management"
        ],
        "summary": "Get specific leave type details",
        "description": "Get detailed information about a specific leave type including policies and rules",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Leave type ID",
            "schema": {
              "type": "integer",
              "example": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Leave type retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "leave_type": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LeaveType"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "description": {
                              "type": "string",
                              "nullable": true,
                              "description": "Description of the leave type",
                              "example": "Annual vacation leave for rest and recreation"
                            },
                            "advance_notice_days": {
                              "type": "integer",
                              "description": "Required advance notice in days",
                              "example": 7
                            },
                            "documentation_required": {
                              "type": "boolean",
                              "description": "Whether documentation is required",
                              "example": false
                            },
                            "documentation_threshold_days": {
                              "type": "integer",
                              "description": "Threshold for requiring documentation",
                              "example": 3
                            },
                            "requires_approval": {
                              "type": "boolean",
                              "description": "Whether requests require approval",
                              "example": true
                            },
                            "deduct_from_balance": {
                              "type": "boolean",
                              "description": "Whether to deduct from balance",
                              "example": true
                            },
                            "allow_negative_balance": {
                              "type": "boolean",
                              "description": "Whether negative balances are allowed",
                              "example": false
                            },
                            "accrual_enabled": {
                              "type": "boolean",
                              "description": "Whether accrual is enabled",
                              "example": true
                            },
                            "accrual_policy": {
                              "type": "object",
                              "nullable": true,
                              "description": "Associated accrual policy",
                              "properties": {
                                "id": {
                                  "type": "integer",
                                  "example": 1
                                },
                                "name": {
                                  "type": "string",
                                  "example": "Standard Vacation Policy"
                                },
                                "accrual_rate": {
                                  "type": "number",
                                  "example": 1.25
                                },
                                "accrual_frequency": {
                                  "type": "string",
                                  "example": "monthly"
                                },
                                "accrual_frequency_display": {
                                  "type": "string",
                                  "example": "Monthly"
                                },
                                "max_accrual": {
                                  "type": "number",
                                  "nullable": true,
                                  "example": 30.0
                                },
                                "carryover_allowed": {
                                  "type": "boolean",
                                  "example": true
                                },
                                "carryover_max": {
                                  "type": "number",
                                  "nullable": true,
                                  "example": 5.0
                                },
                                "waiting_period_days": {
                                  "type": "integer",
                                  "example": 90
                                },
                                "proration_enabled": {
                                  "type": "boolean",
                                  "example": true
                                }
                              }
                            },
                            "business_rules": {
                              "type": "object",
                              "description": "Business rules and policies",
                              "properties": {
                                "advance_notice_required": {
                                  "type": "boolean",
                                  "example": true
                                },
                                "documentation_rules": {
                                  "type": "object",
                                  "properties": {
                                    "required": {
                                      "type": "boolean",
                                      "example": false
                                    },
                                    "threshold_days": {
                                      "type": "integer",
                                      "example": 3
                                    }
                                  }
                                },
                                "approval_workflow": {
                                  "type": "object",
                                  "properties": {
                                    "requires_approval": {
                                      "type": "boolean",
                                      "example": true
                                    },
                                    "auto_approve_threshold": {
                                      "type": "number",
                                      "nullable": true,
                                      "example": null
                                    }
                                  }
                                },
                                "balance_rules": {
                                  "type": "object",
                                  "properties": {
                                    "deduct_from_balance": {
                                      "type": "boolean",
                                      "example": true
                                    },
                                    "allow_negative": {
                                      "type": "boolean",
                                      "example": false
                                    }
                                  }
                                }
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/holidays": {
      "get": {
        "tags": [
          "Leave Management"
        ],
        "summary": "List business holidays",
        "description": "Get all holidays for the business with optional filtering by year, date range, or upcoming holidays.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "year",
            "in": "query",
            "description": "Filter holidays by year",
            "schema": {
              "type": "integer",
              "example": 2024
            }
          },
          {
            "name": "start_date",
            "in": "query",
            "description": "Filter holidays from this date",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2024-01-01"
            }
          },
          {
            "name": "end_date",
            "in": "query",
            "description": "Filter holidays until this date",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2024-12-31"
            }
          },
          {
            "name": "upcoming",
            "in": "query",
            "description": "Only show upcoming holidays",
            "schema": {
              "type": "boolean",
              "example": true
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Holidays retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "Business holiday information",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "description": "Unique holiday ID",
                            "example": 1
                          },
                          "name": {
                            "type": "string",
                            "description": "Holiday name",
                            "example": "New Year's Day"
                          },
                          "date": {
                            "type": "string",
                            "format": "date",
                            "description": "Holiday date",
                            "example": "2024-01-01"
                          },
                          "description": {
                            "type": "string",
                            "nullable": true,
                            "description": "Holiday description",
                            "example": "New Year's Day celebration"
                          },
                          "recurring": {
                            "type": "boolean",
                            "description": "Whether this holiday recurs annually",
                            "example": true
                          },
                          "business_id": {
                            "type": "integer",
                            "description": "Business ID",
                            "example": 1
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2024-01-01T00:00:00Z"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2024-01-01T00:00:00Z"
                          }
                        },
                        "required": [
                          "id",
                          "name",
                          "date",
                          "business_id",
                          "created_at",
                          "updated_at"
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/holidays/{year}": {
      "get": {
        "tags": [
          "Leave Management"
        ],
        "summary": "Get holidays for specific year",
        "description": "Get all holidays for a specific year",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "year",
            "in": "path",
            "required": true,
            "description": "Year to get holidays for",
            "schema": {
              "type": "integer",
              "example": 2024
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Holidays for year retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "year": {
                      "type": "integer",
                      "example": 2024
                    },
                    "holidays": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "Business holiday information",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "description": "Unique holiday ID",
                            "example": 1
                          },
                          "name": {
                            "type": "string",
                            "description": "Holiday name",
                            "example": "New Year's Day"
                          },
                          "date": {
                            "type": "string",
                            "format": "date",
                            "description": "Holiday date",
                            "example": "2024-01-01"
                          },
                          "description": {
                            "type": "string",
                            "nullable": true,
                            "description": "Holiday description",
                            "example": "New Year's Day celebration"
                          },
                          "recurring": {
                            "type": "boolean",
                            "description": "Whether this holiday recurs annually",
                            "example": true
                          },
                          "business_id": {
                            "type": "integer",
                            "description": "Business ID",
                            "example": 1
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2024-01-01T00:00:00Z"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2024-01-01T00:00:00Z"
                          }
                        },
                        "required": [
                          "id",
                          "name",
                          "date",
                          "business_id",
                          "created_at",
                          "updated_at"
                        ]
                      }
                    },
                    "total_count": {
                      "type": "integer",
                      "example": 12
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/holidays/{id}": {
      "get": {
        "tags": [
          "Leave Management"
        ],
        "summary": "Get specific holiday details",
        "description": "Get detailed information about a specific holiday",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Holiday ID",
            "schema": {
              "type": "integer",
              "example": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Holiday retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "holiday": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Holiday"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "day_of_week": {
                              "type": "string",
                              "description": "Day of the week",
                              "example": "Monday"
                            },
                            "week_of_year": {
                              "type": "integer",
                              "description": "Week number of the year",
                              "example": 1
                            },
                            "month_name": {
                              "type": "string",
                              "description": "Month name",
                              "example": "January"
                            },
                            "is_weekend": {
                              "type": "boolean",
                              "description": "Whether the holiday falls on a weekend",
                              "example": false
                            },
                            "days_until": {
                              "type": "integer",
                              "nullable": true,
                              "description": "Days until this holiday (null if past)",
                              "example": 30
                            },
                            "metadata": {
                              "type": "object",
                              "description": "Additional holiday metadata",
                              "properties": {
                                "year": {
                                  "type": "integer",
                                  "example": 2024
                                },
                                "month": {
                                  "type": "integer",
                                  "example": 1
                                },
                                "day": {
                                  "type": "integer",
                                  "example": 1
                                },
                                "quarter": {
                                  "type": "integer",
                                  "example": 1
                                },
                                "is_past": {
                                  "type": "boolean",
                                  "example": false
                                },
                                "is_today": {
                                  "type": "boolean",
                                  "example": false
                                },
                                "is_future": {
                                  "type": "boolean",
                                  "example": true
                                }
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/blackout_periods": {
      "get": {
        "tags": [
          "Leave Management"
        ],
        "summary": "List blackout periods",
        "description": "Get blackout periods with optional filtering by status, date range, leave type, or location.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "Filter by status",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "upcoming",
                "past"
              ],
              "example": "active"
            }
          },
          {
            "name": "start_date",
            "in": "query",
            "description": "Filter periods overlapping from this date",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2024-01-01"
            }
          },
          {
            "name": "end_date",
            "in": "query",
            "description": "Filter periods overlapping until this date",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2024-12-31"
            }
          },
          {
            "name": "leave_type_id",
            "in": "query",
            "description": "Filter by leave type",
            "schema": {
              "type": "integer",
              "example": 1
            }
          },
          {
            "name": "location_id",
            "in": "query",
            "description": "Filter by location",
            "schema": {
              "type": "integer",
              "example": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Blackout periods retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "Leave blackout period information",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "description": "Unique blackout period ID",
                            "example": 1
                          },
                          "name": {
                            "type": "string",
                            "description": "Blackout period name",
                            "example": "Winter Holiday Season"
                          },
                          "description": {
                            "type": "string",
                            "nullable": true,
                            "description": "Blackout period description",
                            "example": "No leave allowed during winter holidays"
                          },
                          "start_date": {
                            "type": "string",
                            "format": "date",
                            "description": "Start date of blackout period",
                            "example": "2024-12-15"
                          },
                          "end_date": {
                            "type": "string",
                            "format": "date",
                            "description": "End date of blackout period",
                            "example": "2024-12-31"
                          },
                          "block_requests": {
                            "type": "boolean",
                            "description": "Whether requests are completely blocked or require special approval",
                            "example": true
                          },
                          "status": {
                            "type": "string",
                            "description": "Current status of the blackout period",
                            "example": "upcoming"
                          },
                          "duration_days": {
                            "type": "integer",
                            "description": "Duration in days",
                            "example": 17
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2024-01-01T00:00:00Z"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2024-01-01T00:00:00Z"
                          }
                        },
                        "required": [
                          "id",
                          "name",
                          "start_date",
                          "end_date",
                          "block_requests",
                          "status",
                          "duration_days"
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/blackout_periods/upcoming": {
      "get": {
        "tags": [
          "Leave Management"
        ],
        "summary": "Get upcoming blackout periods",
        "description": "Get upcoming blackout periods with optional limit",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of periods to return",
            "schema": {
              "type": "integer",
              "default": 10,
              "example": 5
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Upcoming blackout periods retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "upcoming_blackouts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "Leave blackout period information",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "description": "Unique blackout period ID",
                            "example": 1
                          },
                          "name": {
                            "type": "string",
                            "description": "Blackout period name",
                            "example": "Winter Holiday Season"
                          },
                          "description": {
                            "type": "string",
                            "nullable": true,
                            "description": "Blackout period description",
                            "example": "No leave allowed during winter holidays"
                          },
                          "start_date": {
                            "type": "string",
                            "format": "date",
                            "description": "Start date of blackout period",
                            "example": "2024-12-15"
                          },
                          "end_date": {
                            "type": "string",
                            "format": "date",
                            "description": "End date of blackout period",
                            "example": "2024-12-31"
                          },
                          "block_requests": {
                            "type": "boolean",
                            "description": "Whether requests are completely blocked or require special approval",
                            "example": true
                          },
                          "status": {
                            "type": "string",
                            "description": "Current status of the blackout period",
                            "example": "upcoming"
                          },
                          "duration_days": {
                            "type": "integer",
                            "description": "Duration in days",
                            "example": 17
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2024-01-01T00:00:00Z"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2024-01-01T00:00:00Z"
                          }
                        },
                        "required": [
                          "id",
                          "name",
                          "start_date",
                          "end_date",
                          "block_requests",
                          "status",
                          "duration_days"
                        ]
                      }
                    },
                    "total_count": {
                      "type": "integer",
                      "example": 3
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/blackout_periods/current": {
      "get": {
        "tags": [
          "Leave Management"
        ],
        "summary": "Get current active blackout periods",
        "description": "Get all currently active blackout periods",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Current blackout periods retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "current_blackouts": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/BlackoutPeriod"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "leave_types": {
                                "type": "array",
                                "description": "Leave types affected by this blackout",
                                "items": {
                                  "$ref": "#/components/schemas/LeaveTypeBasic"
                                }
                              },
                              "locations": {
                                "type": "array",
                                "description": "Locations affected by this blackout",
                                "items": {
                                  "type": "object",
                                  "properties": {
                                    "id": {
                                      "type": "integer",
                                      "example": 1
                                    },
                                    "name": {
                                      "type": "string",
                                      "example": "Main Office"
                                    },
                                    "address": {
                                      "type": "string",
                                      "example": "123 Main St"
                                    }
                                  }
                                }
                              },
                              "applies_to_all_leave_types": {
                                "type": "boolean",
                                "description": "Whether this applies to all leave types",
                                "example": true
                              },
                              "applies_to_all_locations": {
                                "type": "boolean",
                                "description": "Whether this applies to all locations",
                                "example": true
                              },
                              "days_until_start": {
                                "type": "integer",
                                "nullable": true,
                                "description": "Days until blackout starts (null if past)",
                                "example": 30
                              },
                              "days_until_end": {
                                "type": "integer",
                                "nullable": true,
                                "description": "Days until blackout ends (null if past)",
                                "example": 47
                              },
                              "is_current": {
                                "type": "boolean",
                                "description": "Whether this blackout is currently active",
                                "example": false
                              },
                              "is_upcoming": {
                                "type": "boolean",
                                "description": "Whether this blackout is upcoming",
                                "example": true
                              },
                              "is_past": {
                                "type": "boolean",
                                "description": "Whether this blackout is in the past",
                                "example": false
                              },
                              "created_by": {
                                "type": "object",
                                "nullable": true,
                                "description": "User who created this blackout period",
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 1
                                  },
                                  "name": {
                                    "type": "string",
                                    "example": "Admin User"
                                  }
                                }
                              },
                              "impact_message": {
                                "type": "string",
                                "description": "Message describing the impact of this blackout",
                                "example": "Leave requests are completely blocked during this period"
                              }
                            }
                          }
                        ]
                      }
                    },
                    "total_count": {
                      "type": "integer",
                      "example": 1
                    },
                    "has_active_blackouts": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/blackout_periods/{id}": {
      "get": {
        "tags": [
          "Leave Management"
        ],
        "summary": "Get specific blackout period details",
        "description": "Get detailed information about a specific blackout period",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Blackout period ID",
            "schema": {
              "type": "integer",
              "example": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Blackout period retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "blackout_period": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/BlackoutPeriod"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "leave_types": {
                              "type": "array",
                              "description": "Leave types affected by this blackout",
                              "items": {
                                "$ref": "#/components/schemas/LeaveTypeBasic"
                              }
                            },
                            "locations": {
                              "type": "array",
                              "description": "Locations affected by this blackout",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 1
                                  },
                                  "name": {
                                    "type": "string",
                                    "example": "Main Office"
                                  },
                                  "address": {
                                    "type": "string",
                                    "example": "123 Main St"
                                  }
                                }
                              }
                            },
                            "applies_to_all_leave_types": {
                              "type": "boolean",
                              "description": "Whether this applies to all leave types",
                              "example": true
                            },
                            "applies_to_all_locations": {
                              "type": "boolean",
                              "description": "Whether this applies to all locations",
                              "example": true
                            },
                            "days_until_start": {
                              "type": "integer",
                              "nullable": true,
                              "description": "Days until blackout starts (null if past)",
                              "example": 30
                            },
                            "days_until_end": {
                              "type": "integer",
                              "nullable": true,
                              "description": "Days until blackout ends (null if past)",
                              "example": 47
                            },
                            "is_current": {
                              "type": "boolean",
                              "description": "Whether this blackout is currently active",
                              "example": false
                            },
                            "is_upcoming": {
                              "type": "boolean",
                              "description": "Whether this blackout is upcoming",
                              "example": true
                            },
                            "is_past": {
                              "type": "boolean",
                              "description": "Whether this blackout is in the past",
                              "example": false
                            },
                            "created_by": {
                              "type": "object",
                              "nullable": true,
                              "description": "User who created this blackout period",
                              "properties": {
                                "id": {
                                  "type": "integer",
                                  "example": 1
                                },
                                "name": {
                                  "type": "string",
                                  "example": "Admin User"
                                }
                              }
                            },
                            "impact_message": {
                              "type": "string",
                              "description": "Message describing the impact of this blackout",
                              "example": "Leave requests are completely blocked during this period"
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/leave_coverage/availability": {
      "get": {
        "tags": [
          "Leave Management"
        ],
        "summary": "Check leave availability for specific date",
        "description": "Check if leave is available for a specific date, considering coverage limits and business rules.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "date",
            "in": "query",
            "required": true,
            "description": "Date to check availability for",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2024-03-15"
            }
          },
          {
            "name": "location_id",
            "in": "query",
            "description": "Location to check coverage for",
            "schema": {
              "type": "integer",
              "example": 1
            }
          },
          {
            "name": "leave_type_id",
            "in": "query",
            "description": "Leave type to check",
            "schema": {
              "type": "integer",
              "example": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Availability check completed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Leave availability check result",
                  "properties": {
                    "date": {
                      "type": "string",
                      "format": "date",
                      "description": "Date checked",
                      "example": "2024-03-15"
                    },
                    "available": {
                      "type": "boolean",
                      "description": "Whether leave is available",
                      "example": true
                    },
                    "unlimited": {
                      "type": "boolean",
                      "description": "Whether coverage is unlimited",
                      "example": false
                    },
                    "current_off": {
                      "type": "integer",
                      "description": "Number of people currently off",
                      "example": 2
                    },
                    "max_allowed": {
                      "type": "integer",
                      "nullable": true,
                      "description": "Maximum people allowed off (null if unlimited)",
                      "example": 5
                    },
                    "available_spots": {
                      "type": "integer",
                      "nullable": true,
                      "description": "Available spots remaining (null if unlimited)",
                      "example": 3
                    },
                    "limit_type": {
                      "type": "string",
                      "description": "Type of coverage limit",
                      "example": "percentage"
                    },
                    "message": {
                      "type": "string",
                      "description": "Human-readable availability message",
                      "example": "3 spots available out of 5 maximum"
                    },
                    "metadata": {
                      "type": "object",
                      "description": "Additional date metadata",
                      "properties": {
                        "day_of_week": {
                          "type": "string",
                          "example": "Friday"
                        },
                        "is_weekend": {
                          "type": "boolean",
                          "example": false
                        },
                        "is_holiday": {
                          "type": "boolean",
                          "example": false
                        },
                        "is_blackout_period": {
                          "type": "boolean",
                          "example": false
                        }
                      }
                    }
                  },
                  "required": [
                    "date",
                    "available",
                    "unlimited",
                    "current_off",
                    "message",
                    "metadata"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/leave_coverage/date_range": {
      "get": {
        "tags": [
          "Leave Management"
        ],
        "summary": "Check availability for date range",
        "description": "Check leave availability for a range of dates with summary statistics.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "start_date",
            "in": "query",
            "required": true,
            "description": "Start date of range",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2024-03-15"
            }
          },
          {
            "name": "end_date",
            "in": "query",
            "required": true,
            "description": "End date of range",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2024-03-17"
            }
          },
          {
            "name": "location_id",
            "in": "query",
            "description": "Location to check coverage for",
            "schema": {
              "type": "integer",
              "example": 1
            }
          },
          {
            "name": "leave_type_id",
            "in": "query",
            "description": "Leave type to check",
            "schema": {
              "type": "integer",
              "example": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Date range availability check completed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Leave coverage for date range",
                  "properties": {
                    "start_date": {
                      "type": "string",
                      "format": "date",
                      "example": "2024-03-15"
                    },
                    "end_date": {
                      "type": "string",
                      "format": "date",
                      "example": "2024-03-17"
                    },
                    "total_days": {
                      "type": "integer",
                      "description": "Total days in range",
                      "example": 3
                    },
                    "coverage_data": {
                      "type": "array",
                      "description": "Coverage data for each day",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/LeaveCoverageAvailability"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "week_of_year": {
                                "type": "integer",
                                "example": 11
                              },
                              "month": {
                                "type": "integer",
                                "example": 3
                              }
                            }
                          }
                        ]
                      }
                    },
                    "summary": {
                      "type": "object",
                      "description": "Summary statistics for the range",
                      "properties": {
                        "total_days": {
                          "type": "integer",
                          "example": 3
                        },
                        "available_days": {
                          "type": "integer",
                          "example": 2
                        },
                        "blocked_days": {
                          "type": "integer",
                          "example": 1
                        },
                        "weekend_days": {
                          "type": "integer",
                          "example": 0
                        },
                        "holiday_days": {
                          "type": "integer",
                          "example": 0
                        },
                        "blackout_days": {
                          "type": "integer",
                          "example": 1
                        },
                        "availability_percentage": {
                          "type": "number",
                          "example": 66.7
                        }
                      }
                    }
                  },
                  "required": [
                    "start_date",
                    "end_date",
                    "total_days",
                    "coverage_data",
                    "summary"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/leave_coverage/alternatives": {
      "get": {
        "tags": [
          "Leave Management"
        ],
        "summary": "Find alternative dates for leave request",
        "description": "Find alternative date ranges when the requested dates are not available.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "start_date",
            "in": "query",
            "required": true,
            "description": "Original start date",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2024-03-15"
            }
          },
          {
            "name": "end_date",
            "in": "query",
            "required": true,
            "description": "Original end date",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2024-03-17"
            }
          },
          {
            "name": "location_id",
            "in": "query",
            "description": "Location to check coverage for",
            "schema": {
              "type": "integer",
              "example": 1
            }
          },
          {
            "name": "leave_type_id",
            "in": "query",
            "description": "Leave type to check",
            "schema": {
              "type": "integer",
              "example": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Alternative dates found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Alternative date suggestions",
                  "properties": {
                    "original_request": {
                      "type": "object",
                      "description": "Original requested dates",
                      "properties": {
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-03-15"
                        },
                        "end_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-03-17"
                        },
                        "duration_days": {
                          "type": "integer",
                          "example": 3
                        }
                      }
                    },
                    "alternatives": {
                      "type": "array",
                      "description": "Alternative date ranges",
                      "items": {
                        "type": "object",
                        "properties": {
                          "start_date": {
                            "type": "string",
                            "format": "date",
                            "example": "2024-03-22"
                          },
                          "end_date": {
                            "type": "string",
                            "format": "date",
                            "example": "2024-03-24"
                          },
                          "duration_days": {
                            "type": "integer",
                            "example": 3
                          },
                          "days_from_original": {
                            "type": "integer",
                            "description": "Days difference from original start date",
                            "example": 7
                          },
                          "has_weekends": {
                            "type": "boolean",
                            "example": true
                          },
                          "has_holidays": {
                            "type": "boolean",
                            "example": false
                          },
                          "business_days": {
                            "type": "integer",
                            "example": 3
                          }
                        }
                      }
                    },
                    "total_alternatives": {
                      "type": "integer",
                      "description": "Number of alternatives found",
                      "example": 5
                    },
                    "search_period": {
                      "type": "object",
                      "description": "Period searched for alternatives",
                      "properties": {
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-03-15"
                        },
                        "end_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-05-14"
                        }
                      }
                    }
                  },
                  "required": [
                    "original_request",
                    "alternatives",
                    "total_alternatives",
                    "search_period"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/leave_coverage/calendar/{year}/{month}": {
      "get": {
        "tags": [
          "Leave Management"
        ],
        "summary": "Get calendar view of leave coverage",
        "description": "Get a monthly calendar view showing leave availability, holidays, and blackout periods.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "year",
            "in": "path",
            "required": true,
            "description": "Year for calendar",
            "schema": {
              "type": "integer",
              "example": 2024
            }
          },
          {
            "name": "month",
            "in": "path",
            "required": true,
            "description": "Month for calendar (1-12)",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 12,
              "example": 3
            }
          },
          {
            "name": "location_id",
            "in": "query",
            "description": "Location to check coverage for",
            "schema": {
              "type": "integer",
              "example": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calendar data retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Monthly calendar view of leave coverage",
                  "properties": {
                    "year": {
                      "type": "integer",
                      "example": 2024
                    },
                    "month": {
                      "type": "integer",
                      "example": 3
                    },
                    "month_name": {
                      "type": "string",
                      "example": "March"
                    },
                    "days_in_month": {
                      "type": "integer",
                      "example": 31
                    },
                    "calendar_data": {
                      "type": "array",
                      "description": "Data for each day of the month",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/LeaveCoverageAvailability"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "day_of_month": {
                                "type": "integer",
                                "example": 15
                              },
                              "day_of_week_short": {
                                "type": "string",
                                "example": "Fri"
                              },
                              "is_today": {
                                "type": "boolean",
                                "example": false
                              },
                              "holiday": {
                                "type": "object",
                                "nullable": true,
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 1
                                  },
                                  "name": {
                                    "type": "string",
                                    "example": "Good Friday"
                                  },
                                  "description": {
                                    "type": "string",
                                    "example": "Christian holiday"
                                  }
                                }
                              },
                              "blackout_period": {
                                "type": "object",
                                "nullable": true,
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 1
                                  },
                                  "name": {
                                    "type": "string",
                                    "example": "Spring Break"
                                  },
                                  "blocks_requests": {
                                    "type": "boolean",
                                    "example": true
                                  }
                                }
                              },
                              "week_of_month": {
                                "type": "integer",
                                "example": 3
                              }
                            }
                          }
                        ]
                      }
                    },
                    "summary": {
                      "type": "object",
                      "description": "Monthly summary statistics",
                      "properties": {
                        "total_days": {
                          "type": "integer",
                          "example": 31
                        },
                        "available_days": {
                          "type": "integer",
                          "example": 25
                        },
                        "blocked_days": {
                          "type": "integer",
                          "example": 6
                        },
                        "weekend_days": {
                          "type": "integer",
                          "example": 8
                        },
                        "holiday_days": {
                          "type": "integer",
                          "example": 2
                        },
                        "blackout_days": {
                          "type": "integer",
                          "example": 4
                        }
                      }
                    }
                  },
                  "required": [
                    "year",
                    "month",
                    "month_name",
                    "days_in_month",
                    "calendar_data",
                    "summary"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/user_availabilities": {
      "get": {
        "tags": [
          "Availability"
        ],
        "summary": "List user availabilities and leave blocks",
        "description": "Get a list of user availability blocks and leave requests for a specific week. \nReturns availability data along with leave blocks (approved and pending), week \nconfirmation status, and calendar settings. This matches the web UI display \nwhere both availability and leave blocks are shown together.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "week_start",
            "in": "query",
            "description": "Week start date (defaults to current week)",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2025-10-13"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Availabilities and leaves retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "availabilities": {
                      "type": "array",
                      "description": "User availability blocks for the week",
                      "items": {
                        "type": "object",
                        "description": "User availability block information",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "description": "Unique availability ID",
                            "example": 123
                          },
                          "specific_date": {
                            "type": "string",
                            "format": "date",
                            "description": "Date for this availability block",
                            "example": "2025-10-15"
                          },
                          "start_time": {
                            "type": "string",
                            "format": "time",
                            "description": "Start time (displayed in user's timezone)",
                            "example": "09:00"
                          },
                          "end_time": {
                            "type": "string",
                            "format": "time",
                            "description": "End time (displayed in user's timezone)",
                            "example": "17:00"
                          },
                          "notes": {
                            "type": "string",
                            "nullable": true,
                            "description": "Optional notes about this availability",
                            "example": "Available for morning shift"
                          },
                          "store_in_utc": {
                            "type": "boolean",
                            "description": "Whether times are stored in UTC",
                            "example": true
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When this availability was created",
                            "example": "2025-10-01T10:30:00Z"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When this availability was last updated",
                            "example": "2025-10-01T10:30:00Z"
                          }
                        },
                        "required": [
                          "id",
                          "specific_date",
                          "start_time",
                          "end_time"
                        ]
                      }
                    },
                    "leaves": {
                      "type": "array",
                      "description": "Leave requests (approved and pending) for the week",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "description": "Leave request ID",
                            "example": 123
                          },
                          "start_date": {
                            "type": "string",
                            "format": "date",
                            "description": "Leave start date",
                            "example": "2025-10-15"
                          },
                          "end_date": {
                            "type": "string",
                            "format": "date",
                            "description": "Leave end date",
                            "example": "2025-10-15"
                          },
                          "leave_type": {
                            "type": "string",
                            "description": "Name of the leave type",
                            "example": "Vacation"
                          },
                          "status": {
                            "type": "string",
                            "description": "Leave request status",
                            "enum": [
                              "approved",
                              "pending"
                            ],
                            "example": "approved"
                          },
                          "reason": {
                            "type": "string",
                            "description": "Reason for leave",
                            "example": "Personal time off"
                          }
                        }
                      }
                    },
                    "week_start": {
                      "type": "string",
                      "format": "date",
                      "example": "2025-10-13"
                    },
                    "week_end": {
                      "type": "string",
                      "format": "date",
                      "example": "2025-10-19"
                    },
                    "week_confirmed": {
                      "type": "boolean",
                      "example": false
                    },
                    "calendar_settings": {
                      "type": "object",
                      "properties": {
                        "start_time": {
                          "type": "integer",
                          "example": 6
                        },
                        "end_time": {
                          "type": "integer",
                          "example": 21
                        },
                        "minimum_block_minutes": {
                          "type": "integer",
                          "example": 60
                        }
                      }
                    },
                    "total_count": {
                      "type": "integer",
                      "description": "Total count of availability blocks",
                      "example": 5
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Availability"
        ],
        "summary": "Create user availability",
        "description": "Create a new availability block for a specific date and time range.\nDate must be editable (not in past or confirmed week).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "specific_date",
                  "start_time",
                  "end_time"
                ],
                "properties": {
                  "specific_date": {
                    "type": "string",
                    "format": "date",
                    "example": "2025-10-15"
                  },
                  "start_time": {
                    "type": "string",
                    "format": "time",
                    "example": "09:00"
                  },
                  "end_time": {
                    "type": "string",
                    "format": "time",
                    "example": "17:00"
                  },
                  "notes": {
                    "type": "string",
                    "example": "Available for morning shift"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Availability created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "availability": {
                      "type": "object",
                      "description": "User availability block information",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "description": "Unique availability ID",
                          "example": 123
                        },
                        "specific_date": {
                          "type": "string",
                          "format": "date",
                          "description": "Date for this availability block",
                          "example": "2025-10-15"
                        },
                        "start_time": {
                          "type": "string",
                          "format": "time",
                          "description": "Start time (displayed in user's timezone)",
                          "example": "09:00"
                        },
                        "end_time": {
                          "type": "string",
                          "format": "time",
                          "description": "End time (displayed in user's timezone)",
                          "example": "17:00"
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true,
                          "description": "Optional notes about this availability",
                          "example": "Available for morning shift"
                        },
                        "store_in_utc": {
                          "type": "boolean",
                          "description": "Whether times are stored in UTC",
                          "example": true
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "When this availability was created",
                          "example": "2025-10-01T10:30:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "When this availability was last updated",
                          "example": "2025-10-01T10:30:00Z"
                        }
                      },
                      "required": [
                        "id",
                        "specific_date",
                        "start_time",
                        "end_time"
                      ]
                    },
                    "message": {
                      "type": "string",
                      "example": "Availability created successfully"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error or duplicate availability",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/user_availabilities/weekly": {
      "get": {
        "tags": [
          "Availability"
        ],
        "summary": "Get weekly availability summary",
        "description": "Get a comprehensive weekly view including availabilities, leaves, \nconfirmation status, and total hours for a specific week.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "week_start",
            "in": "query",
            "description": "Week start date (defaults to current week)",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2025-10-13"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Weekly availability retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "week_start": {
                      "type": "string",
                      "format": "date",
                      "example": "2025-10-13"
                    },
                    "week_end": {
                      "type": "string",
                      "format": "date",
                      "example": "2025-10-19"
                    },
                    "availabilities": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "User availability block information",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "description": "Unique availability ID",
                            "example": 123
                          },
                          "specific_date": {
                            "type": "string",
                            "format": "date",
                            "description": "Date for this availability block",
                            "example": "2025-10-15"
                          },
                          "start_time": {
                            "type": "string",
                            "format": "time",
                            "description": "Start time (displayed in user's timezone)",
                            "example": "09:00"
                          },
                          "end_time": {
                            "type": "string",
                            "format": "time",
                            "description": "End time (displayed in user's timezone)",
                            "example": "17:00"
                          },
                          "notes": {
                            "type": "string",
                            "nullable": true,
                            "description": "Optional notes about this availability",
                            "example": "Available for morning shift"
                          },
                          "store_in_utc": {
                            "type": "boolean",
                            "description": "Whether times are stored in UTC",
                            "example": true
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When this availability was created",
                            "example": "2025-10-01T10:30:00Z"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When this availability was last updated",
                            "example": "2025-10-01T10:30:00Z"
                          }
                        },
                        "required": [
                          "id",
                          "specific_date",
                          "start_time",
                          "end_time"
                        ]
                      }
                    },
                    "leaves": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "Basic leave request information",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "description": "Unique leave request ID",
                            "example": 123
                          },
                          "user_id": {
                            "type": "integer",
                            "description": "ID of the user who created the request",
                            "example": 456
                          },
                          "leave_type": {
                            "$ref": "#/components/schemas/LeaveTypeBasic"
                          },
                          "start_date": {
                            "type": "string",
                            "format": "date",
                            "description": "Start date of leave",
                            "example": "2024-03-15"
                          },
                          "end_date": {
                            "type": "string",
                            "format": "date",
                            "description": "End date of leave",
                            "example": "2024-03-17"
                          },
                          "hours_calculated": {
                            "type": "number",
                            "description": "Total hours for this leave request (primary field)",
                            "example": 24
                          },
                          "business_days": {
                            "type": "number",
                            "description": "DEPRECATED: Use hours_calculated instead. Returns same value as hours_calculated for backward compatibility.",
                            "example": 24
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "pending",
                              "approved",
                              "denied",
                              "cancelled",
                              "special_approval"
                            ],
                            "description": "Current status of the leave request",
                            "example": "pending"
                          },
                          "notes": {
                            "type": "string",
                            "nullable": true,
                            "description": "Optional notes for the leave request",
                            "example": "Family vacation"
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When the request was created",
                            "example": "2024-02-15T10:00:00Z"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When the request was last updated",
                            "example": "2024-02-15T10:00:00Z"
                          }
                        },
                        "required": [
                          "id",
                          "user_id",
                          "leave_type",
                          "start_date",
                          "end_date",
                          "hours_calculated",
                          "status",
                          "created_at",
                          "updated_at"
                        ]
                      }
                    },
                    "confirmed": {
                      "type": "boolean",
                      "example": false
                    },
                    "confirmed_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "total_hours": {
                      "type": "number",
                      "format": "float",
                      "example": 40.0
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/user_availabilities/bulk_create": {
      "post": {
        "tags": [
          "Availability"
        ],
        "summary": "Bulk create user availabilities",
        "description": "Create multiple availability blocks in a single request. \nReturns success and failure results for each item.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "availabilities"
                ],
                "properties": {
                  "availabilities": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "specific_date",
                        "start_time",
                        "end_time"
                      ],
                      "properties": {
                        "specific_date": {
                          "type": "string",
                          "format": "date"
                        },
                        "start_time": {
                          "type": "string",
                          "format": "time"
                        },
                        "end_time": {
                          "type": "string",
                          "format": "time"
                        },
                        "notes": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Bulk operation completed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "object",
                      "properties": {
                        "success": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "description": "User availability block information",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "description": "Unique availability ID",
                                "example": 123
                              },
                              "specific_date": {
                                "type": "string",
                                "format": "date",
                                "description": "Date for this availability block",
                                "example": "2025-10-15"
                              },
                              "start_time": {
                                "type": "string",
                                "format": "time",
                                "description": "Start time (displayed in user's timezone)",
                                "example": "09:00"
                              },
                              "end_time": {
                                "type": "string",
                                "format": "time",
                                "description": "End time (displayed in user's timezone)",
                                "example": "17:00"
                              },
                              "notes": {
                                "type": "string",
                                "nullable": true,
                                "description": "Optional notes about this availability",
                                "example": "Available for morning shift"
                              },
                              "store_in_utc": {
                                "type": "boolean",
                                "description": "Whether times are stored in UTC",
                                "example": true
                              },
                              "created_at": {
                                "type": "string",
                                "format": "date-time",
                                "description": "When this availability was created",
                                "example": "2025-10-01T10:30:00Z"
                              },
                              "updated_at": {
                                "type": "string",
                                "format": "date-time",
                                "description": "When this availability was last updated",
                                "example": "2025-10-01T10:30:00Z"
                              }
                            },
                            "required": [
                              "id",
                              "specific_date",
                              "start_time",
                              "end_time"
                            ]
                          }
                        },
                        "failed": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "date": {
                                "type": "string"
                              },
                              "error": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      }
                    },
                    "success_count": {
                      "type": "integer"
                    },
                    "failed_count": {
                      "type": "integer"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/user_availabilities/mark_available_all_week": {
      "post": {
        "tags": [
          "Availability"
        ],
        "summary": "Mark available for entire week",
        "description": "Mark the user as available for an entire week by creating availability blocks\nfor each day. Any existing availability for the week is cleared first, then new\nblocks are created based on the business's minimum block size setting.\n\nDays with approved leave are automatically skipped. Past dates within the week\nare also skipped. Weekends are excluded by default unless `include_weekends` is true.\n\nFor the current day, if the requested start time has already passed, availability\nbegins from the next hour.\n",
        "operationId": "markAvailableAllWeekUserAvailabilities",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "week_start": {
                    "type": "string",
                    "format": "date",
                    "description": "Start date of the week (YYYY-MM-DD). Defaults to the current week.",
                    "example": "2026-03-02"
                  },
                  "start_time": {
                    "type": "string",
                    "description": "Daily start time in HH:MM format. Defaults to \"00:00\".",
                    "example": "08:00"
                  },
                  "end_time": {
                    "type": "string",
                    "description": "Daily end time in HH:MM format. Defaults to \"23:59\".",
                    "example": "17:00"
                  },
                  "include_weekends": {
                    "type": "boolean",
                    "description": "Whether to include Saturday and Sunday. Defaults to false.",
                    "default": false,
                    "example": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Week availability created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "Human-readable success message",
                      "example": "Successfully marked available for the week of March 02, 2026."
                    },
                    "week_start": {
                      "type": "string",
                      "format": "date",
                      "description": "Start date of the week",
                      "example": "2026-03-02"
                    },
                    "week_end": {
                      "type": "string",
                      "format": "date",
                      "description": "End date of the week",
                      "example": "2026-03-08"
                    },
                    "created_count": {
                      "type": "integer",
                      "description": "Number of new availability blocks created",
                      "example": 30
                    },
                    "cleared_count": {
                      "type": "integer",
                      "description": "Number of existing blocks that were cleared",
                      "example": 0
                    },
                    "skipped_leave_count": {
                      "type": "integer",
                      "description": "Number of days skipped due to approved leave",
                      "example": 0
                    },
                    "availabilities": {
                      "type": "array",
                      "description": "All availability blocks for the week after the operation",
                      "items": {
                        "type": "object",
                        "description": "User availability block information",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "description": "Unique availability ID",
                            "example": 123
                          },
                          "specific_date": {
                            "type": "string",
                            "format": "date",
                            "description": "Date for this availability block",
                            "example": "2025-10-15"
                          },
                          "start_time": {
                            "type": "string",
                            "format": "time",
                            "description": "Start time (displayed in user's timezone)",
                            "example": "09:00"
                          },
                          "end_time": {
                            "type": "string",
                            "format": "time",
                            "description": "End time (displayed in user's timezone)",
                            "example": "17:00"
                          },
                          "notes": {
                            "type": "string",
                            "nullable": true,
                            "description": "Optional notes about this availability",
                            "example": "Available for morning shift"
                          },
                          "store_in_utc": {
                            "type": "boolean",
                            "description": "Whether times are stored in UTC",
                            "example": true
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When this availability was created",
                            "example": "2025-10-01T10:30:00Z"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When this availability was last updated",
                            "example": "2025-10-01T10:30:00Z"
                          }
                        },
                        "required": [
                          "id",
                          "specific_date",
                          "start_time",
                          "end_time"
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid date or time format",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                },
                "examples": {
                  "invalid_date": {
                    "summary": "Invalid date format",
                    "value": {
                      "error": {
                        "code": "invalid_date",
                        "message": "Invalid week_start date format"
                      }
                    }
                  },
                  "invalid_time": {
                    "summary": "Invalid time format",
                    "value": {
                      "error": {
                        "code": "invalid_time",
                        "message": "Invalid start_time or end_time format. Use HH:MM."
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Week is confirmed or in the past",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                },
                "examples": {
                  "week_confirmed": {
                    "summary": "Week is confirmed",
                    "value": {
                      "error": {
                        "code": "week_confirmed",
                        "message": "Cannot modify availability for confirmed weeks."
                      }
                    }
                  },
                  "past_week": {
                    "summary": "Week is in the past",
                    "value": {
                      "error": {
                        "code": "past_week",
                        "message": "Cannot modify availability for past weeks."
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/user_availabilities/clear_week": {
      "delete": {
        "tags": [
          "Availability"
        ],
        "summary": "Clear all availability for a week",
        "description": "Delete all availability blocks for the specified week. The week must not be\nconfirmed and must not be in the past. Use this to reset a user's availability\nfor an entire week in a single operation.\n",
        "operationId": "clearWeekUserAvailabilities",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "week_start",
            "in": "query",
            "description": "Start date of the week to clear (YYYY-MM-DD). Defaults to the current\nbusiness week if not provided.\n",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2025-10-13"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Week availability cleared successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "Human-readable success message",
                      "example": "Successfully cleared 5 availability blocks for the week of October 13, 2025."
                    },
                    "cleared_count": {
                      "type": "integer",
                      "description": "Number of availability blocks that were deleted",
                      "example": 5
                    },
                    "week_start": {
                      "type": "string",
                      "format": "date",
                      "description": "Start date of the cleared week",
                      "example": "2025-10-13"
                    },
                    "week_end": {
                      "type": "string",
                      "format": "date",
                      "description": "End date of the cleared week",
                      "example": "2025-10-19"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid date format",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                },
                "example": {
                  "error": {
                    "code": "invalid_date",
                    "message": "Invalid week_start date format"
                  }
                }
              }
            }
          },
          "404": {
            "description": "No availability blocks found for the week",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                },
                "example": {
                  "error": {
                    "code": "no_availability",
                    "message": "No availability blocks found for the week of October 13, 2025."
                  }
                }
              }
            }
          },
          "422": {
            "description": "Week is confirmed or in the past",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                },
                "examples": {
                  "week_confirmed": {
                    "summary": "Week is confirmed",
                    "value": {
                      "error": {
                        "code": "week_confirmed",
                        "message": "Cannot clear availability for confirmed weeks. Unconfirm the week first."
                      }
                    }
                  },
                  "past_week": {
                    "summary": "Week is in the past",
                    "value": {
                      "error": {
                        "code": "past_week",
                        "message": "Cannot clear availability for past weeks."
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/user_availabilities/copy_week_to_week": {
      "post": {
        "tags": [
          "Availability"
        ],
        "summary": "Copy availability from one week to another",
        "description": "Copy all availability blocks from a source week to a target week. Existing\navailability in the target week is preserved — duplicate blocks are skipped\nrather than overwritten.\n\nDays with approved leave in the target week are automatically skipped.\nThe target week must not be confirmed.\n",
        "operationId": "copyWeekToWeekUserAvailabilities",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "source_week_start",
                  "target_week_start"
                ],
                "properties": {
                  "source_week_start": {
                    "type": "string",
                    "format": "date",
                    "description": "Start date of the source week to copy from (YYYY-MM-DD).",
                    "example": "2026-03-02"
                  },
                  "target_week_start": {
                    "type": "string",
                    "format": "date",
                    "description": "Start date of the target week to copy to (YYYY-MM-DD).",
                    "example": "2026-03-09"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Availability copied successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "description": "Whether the operation succeeded",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "description": "Human-readable success message",
                      "example": "Successfully copied 5 availability blocks"
                    },
                    "copied_count": {
                      "type": "integer",
                      "description": "Number of availability blocks copied",
                      "example": 5
                    },
                    "duplicate_count": {
                      "type": "integer",
                      "description": "Number of blocks skipped because they already existed in the target week",
                      "example": 0
                    },
                    "skipped_leave_count": {
                      "type": "integer",
                      "description": "Number of blocks skipped because of approved leave in the target week",
                      "example": 1
                    },
                    "source_week_start": {
                      "type": "string",
                      "format": "date",
                      "description": "Start date of the source week",
                      "example": "2026-03-02"
                    },
                    "target_week_start": {
                      "type": "string",
                      "format": "date",
                      "description": "Start date of the target week",
                      "example": "2026-03-09"
                    },
                    "availabilities": {
                      "type": "array",
                      "description": "All availability blocks for the target week after the operation",
                      "items": {
                        "type": "object",
                        "description": "User availability block information",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "description": "Unique availability ID",
                            "example": 123
                          },
                          "specific_date": {
                            "type": "string",
                            "format": "date",
                            "description": "Date for this availability block",
                            "example": "2025-10-15"
                          },
                          "start_time": {
                            "type": "string",
                            "format": "time",
                            "description": "Start time (displayed in user's timezone)",
                            "example": "09:00"
                          },
                          "end_time": {
                            "type": "string",
                            "format": "time",
                            "description": "End time (displayed in user's timezone)",
                            "example": "17:00"
                          },
                          "notes": {
                            "type": "string",
                            "nullable": true,
                            "description": "Optional notes about this availability",
                            "example": "Available for morning shift"
                          },
                          "store_in_utc": {
                            "type": "boolean",
                            "description": "Whether times are stored in UTC",
                            "example": true
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When this availability was created",
                            "example": "2025-10-01T10:30:00Z"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When this availability was last updated",
                            "example": "2025-10-01T10:30:00Z"
                          }
                        },
                        "required": [
                          "id",
                          "specific_date",
                          "start_time",
                          "end_time"
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid date format",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                },
                "example": {
                  "error": {
                    "code": "invalid_date",
                    "message": "Invalid source_week_start date format. Use YYYY-MM-DD."
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error (same week, confirmed, or past)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                },
                "examples": {
                  "same_week": {
                    "summary": "Source and target are the same week",
                    "value": {
                      "error": {
                        "code": "same_week",
                        "message": "Source and target weeks must be different."
                      }
                    }
                  },
                  "week_confirmed": {
                    "summary": "Target week is confirmed",
                    "value": {
                      "error": {
                        "code": "week_confirmed",
                        "message": "Cannot copy availability to a confirmed week. Unconfirm the target week first."
                      }
                    }
                  },
                  "past_week": {
                    "summary": "Target week is in the past",
                    "value": {
                      "error": {
                        "code": "past_week",
                        "message": "Cannot copy availability to a past week."
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/user_availabilities/{id}": {
      "get": {
        "tags": [
          "Availability"
        ],
        "summary": "Get specific availability",
        "description": "Get details of a specific availability block",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Availability retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "availability": {
                      "type": "object",
                      "description": "User availability block information",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "description": "Unique availability ID",
                          "example": 123
                        },
                        "specific_date": {
                          "type": "string",
                          "format": "date",
                          "description": "Date for this availability block",
                          "example": "2025-10-15"
                        },
                        "start_time": {
                          "type": "string",
                          "format": "time",
                          "description": "Start time (displayed in user's timezone)",
                          "example": "09:00"
                        },
                        "end_time": {
                          "type": "string",
                          "format": "time",
                          "description": "End time (displayed in user's timezone)",
                          "example": "17:00"
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true,
                          "description": "Optional notes about this availability",
                          "example": "Available for morning shift"
                        },
                        "store_in_utc": {
                          "type": "boolean",
                          "description": "Whether times are stored in UTC",
                          "example": true
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "When this availability was created",
                          "example": "2025-10-01T10:30:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "When this availability was last updated",
                          "example": "2025-10-01T10:30:00Z"
                        }
                      },
                      "required": [
                        "id",
                        "specific_date",
                        "start_time",
                        "end_time"
                      ]
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Availability"
        ],
        "summary": "Update user availability",
        "description": "Update an existing availability block. Date must still be editable.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "start_time": {
                    "type": "string",
                    "format": "time",
                    "example": "09:00"
                  },
                  "end_time": {
                    "type": "string",
                    "format": "time",
                    "example": "17:00"
                  },
                  "notes": {
                    "type": "string",
                    "example": "Updated availability"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Availability updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "availability": {
                      "type": "object",
                      "description": "User availability block information",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "description": "Unique availability ID",
                          "example": 123
                        },
                        "specific_date": {
                          "type": "string",
                          "format": "date",
                          "description": "Date for this availability block",
                          "example": "2025-10-15"
                        },
                        "start_time": {
                          "type": "string",
                          "format": "time",
                          "description": "Start time (displayed in user's timezone)",
                          "example": "09:00"
                        },
                        "end_time": {
                          "type": "string",
                          "format": "time",
                          "description": "End time (displayed in user's timezone)",
                          "example": "17:00"
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true,
                          "description": "Optional notes about this availability",
                          "example": "Available for morning shift"
                        },
                        "store_in_utc": {
                          "type": "boolean",
                          "description": "Whether times are stored in UTC",
                          "example": true
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "When this availability was created",
                          "example": "2025-10-01T10:30:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "When this availability was last updated",
                          "example": "2025-10-01T10:30:00Z"
                        }
                      },
                      "required": [
                        "id",
                        "specific_date",
                        "start_time",
                        "end_time"
                      ]
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Validation error or date not editable",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Availability"
        ],
        "summary": "Delete user availability",
        "description": "Delete an availability block. Date must still be editable.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Availability deleted successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Date not editable",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/weekly_availability_confirmations/status": {
      "get": {
        "tags": [
          "Availability"
        ],
        "summary": "Get weekly confirmation status",
        "description": "Check if a specific week's availability has been confirmed by the user.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "week_start",
            "in": "query",
            "description": "Week start date (defaults to current week)",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2025-10-13"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Confirmation status retrieved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "week_start": {
                      "type": "string",
                      "format": "date"
                    },
                    "week_end": {
                      "type": "string",
                      "format": "date"
                    },
                    "confirmed": {
                      "type": "boolean"
                    },
                    "confirmed_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "notes": {
                      "type": "string",
                      "nullable": true
                    },
                    "auto_generated": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/weekly_availability_confirmations": {
      "post": {
        "tags": [
          "Availability"
        ],
        "summary": "Confirm weekly availability",
        "description": "Confirm user's availability for a specific week. This locks the week from editing\nand signals to managers that the schedule is final.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "week_start"
                ],
                "properties": {
                  "week_start": {
                    "type": "string",
                    "format": "date",
                    "example": "2025-10-13"
                  },
                  "notes": {
                    "type": "string",
                    "example": "Confirmed for this week"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Availability confirmed successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "confirmation": {
                      "type": "object",
                      "description": "Weekly availability confirmation information",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "description": "Unique confirmation ID",
                          "example": 456
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "description": "Week start date",
                          "example": "2025-10-13"
                        },
                        "end_date": {
                          "type": "string",
                          "format": "date",
                          "description": "Week end date",
                          "example": "2025-10-19"
                        },
                        "confirmed_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "When the availability was confirmed",
                          "example": "2025-10-12T15:30:00Z"
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true,
                          "description": "Optional notes about the confirmation",
                          "example": "Confirmed for this week"
                        },
                        "auto_generated": {
                          "type": "boolean",
                          "description": "Whether this was auto-generated by the system",
                          "example": false
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "When this confirmation was created",
                          "example": "2025-10-12T15:30:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "When this confirmation was last updated",
                          "example": "2025-10-12T15:30:00Z"
                        }
                      },
                      "required": [
                        "id",
                        "start_date",
                        "end_date",
                        "confirmed_at",
                        "auto_generated"
                      ]
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Cannot confirm past week or validation error",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Availability"
        ],
        "summary": "Unconfirm weekly availability",
        "description": "Remove the confirmation for a specific week, allowing the user to edit their availability again.\nCannot unconfirm past weeks.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "week_start"
                ],
                "properties": {
                  "week_start": {
                    "type": "string",
                    "format": "date",
                    "example": "2025-10-13"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Confirmation removed successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "week_start": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Cannot unconfirm past week",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine readable error code"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human readable error message"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context"
                        }
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/skills": {
      "get": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "List available skills",
        "description": "Get a list of skills available in the business. Supports filtering, searching, and sorting.\n\n**Use Cases:**\n- Browse skills for adding to employee profile\n- Search for specific skills\n- Filter by category or certification requirements\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "description": "Search skills by name or description",
            "schema": {
              "type": "string",
              "example": "javascript"
            }
          },
          {
            "name": "category",
            "in": "query",
            "description": "Filter by skill category",
            "schema": {
              "type": "string",
              "example": "technical_skills"
            }
          },
          {
            "name": "requires_certification",
            "in": "query",
            "description": "Filter by certification requirement",
            "schema": {
              "type": "boolean",
              "example": true
            }
          },
          {
            "name": "sort_by",
            "in": "query",
            "description": "Sort skills by field",
            "schema": {
              "type": "string",
              "enum": [
                "name",
                "category",
                "popularity"
              ],
              "example": "name"
            }
          },
          {
            "name": "sort_order",
            "in": "query",
            "description": "Sort direction. For `name`/`category` the default is ascending. For `popularity`, results default to most-popular-first; `sort_order=asc` lists least-popular first.",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "asc",
              "example": "desc"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "example": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Number of items per page",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "example": 25
            }
          },
          {
            "name": "detailed",
            "in": "query",
            "description": "When `true`, each item additionally includes `employee_count` (number of active employees holding the skill), `average_proficiency`, `metadata`, and `help_desk_tier`.\n",
            "schema": {
              "type": "boolean",
              "example": true
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Skills retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "oneOf": [
                          {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 123
                              },
                              "name": {
                                "type": "string",
                                "example": "JavaScript Development"
                              },
                              "description": {
                                "type": "string",
                                "nullable": true,
                                "example": "Frontend and backend JavaScript programming"
                              },
                              "category": {
                                "type": "string",
                                "nullable": true,
                                "example": "technical_skills"
                              },
                              "category_display": {
                                "type": "string",
                                "nullable": true,
                                "example": "Technical Skills"
                              },
                              "requires_certification": {
                                "type": "boolean",
                                "example": true
                              },
                              "skill_kind": {
                                "type": "string",
                                "enum": [
                                  "skill",
                                  "certification",
                                  "license"
                                ],
                                "description": "Catalog credential type. Clients map `license` -> LIC badge, `certification` -> CERT badge, `skill` -> no badge.\n",
                                "example": "certification"
                              },
                              "requires_document_upload": {
                                "type": "boolean",
                                "example": false
                              },
                              "active": {
                                "type": "boolean",
                                "example": true
                              },
                              "created_at": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2024-01-15T10:00:00Z"
                              },
                              "updated_at": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2024-01-15T10:00:00Z"
                              }
                            }
                          },
                          {
                            "allOf": [
                              {
                                "$ref": "#/components/schemas/Skill"
                              },
                              {
                                "type": "object",
                                "properties": {
                                  "metadata": {
                                    "type": "object",
                                    "nullable": true,
                                    "description": "Additional skill metadata"
                                  },
                                  "help_desk_tier": {
                                    "type": "integer",
                                    "nullable": true,
                                    "example": 2
                                  },
                                  "employee_count": {
                                    "type": "integer",
                                    "example": 15
                                  },
                                  "average_proficiency": {
                                    "type": "number",
                                    "format": "float",
                                    "nullable": true,
                                    "example": 3.4
                                  }
                                }
                              }
                            ]
                          }
                        ],
                        "description": "`SkillDetailed` (with `employee_count`) is returned when `?detailed=true`; otherwise the base `Skill` schema.\n"
                      }
                    },
                    "total_count": {
                      "type": "integer",
                      "description": "Total number of skills matching the query (across all pages)",
                      "example": 81
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/skills/{id}": {
      "get": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "Get skill details",
        "description": "Get detailed information about a specific skill",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Skill ID",
            "schema": {
              "type": "integer",
              "example": 123
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Skill details retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "skill": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Skill"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "metadata": {
                              "type": "object",
                              "nullable": true,
                              "description": "Additional skill metadata"
                            },
                            "help_desk_tier": {
                              "type": "integer",
                              "nullable": true,
                              "example": 2
                            },
                            "employee_count": {
                              "type": "integer",
                              "example": 15
                            },
                            "average_proficiency": {
                              "type": "number",
                              "format": "float",
                              "nullable": true,
                              "example": 3.4
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/skills/categories": {
      "get": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "Get skill categories",
        "description": "Get list of skill categories with counts",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Categories retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "categories": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "value": {
                            "type": "string",
                            "example": "technical_skills"
                          },
                          "label": {
                            "type": "string",
                            "example": "Technical Skills"
                          },
                          "skill_count": {
                            "type": "integer",
                            "example": 25
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/skills/trending": {
      "get": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "Get trending skills",
        "description": "Get skills that have been added frequently by employees recently",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Trending skills retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "trending_skills": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 123
                              },
                              "name": {
                                "type": "string",
                                "example": "JavaScript Development"
                              },
                              "description": {
                                "type": "string",
                                "nullable": true,
                                "example": "Frontend and backend JavaScript programming"
                              },
                              "category": {
                                "type": "string",
                                "nullable": true,
                                "example": "technical_skills"
                              },
                              "category_display": {
                                "type": "string",
                                "nullable": true,
                                "example": "Technical Skills"
                              },
                              "requires_certification": {
                                "type": "boolean",
                                "example": true
                              },
                              "skill_kind": {
                                "type": "string",
                                "enum": [
                                  "skill",
                                  "certification",
                                  "license"
                                ],
                                "description": "Catalog credential type. Clients map `license` -> LIC badge, `certification` -> CERT badge, `skill` -> no badge.\n",
                                "example": "certification"
                              },
                              "requires_document_upload": {
                                "type": "boolean",
                                "example": false
                              },
                              "active": {
                                "type": "boolean",
                                "example": true
                              },
                              "created_at": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2024-01-15T10:00:00Z"
                              },
                              "updated_at": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2024-01-15T10:00:00Z"
                              }
                            }
                          },
                          {
                            "type": "object",
                            "properties": {
                              "recent_additions": {
                                "type": "integer",
                                "example": 8
                              },
                              "total_employees": {
                                "type": "integer",
                                "example": 15
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/skills/recommended": {
      "get": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "Get recommended skills",
        "description": "Get AI-recommended skills for the current user based on role, peers, and career progression",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of recommendations",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 20,
              "example": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Recommended skills retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "recommended_skills": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 123
                              },
                              "name": {
                                "type": "string",
                                "example": "JavaScript Development"
                              },
                              "description": {
                                "type": "string",
                                "nullable": true,
                                "example": "Frontend and backend JavaScript programming"
                              },
                              "category": {
                                "type": "string",
                                "nullable": true,
                                "example": "technical_skills"
                              },
                              "category_display": {
                                "type": "string",
                                "nullable": true,
                                "example": "Technical Skills"
                              },
                              "requires_certification": {
                                "type": "boolean",
                                "example": true
                              },
                              "skill_kind": {
                                "type": "string",
                                "enum": [
                                  "skill",
                                  "certification",
                                  "license"
                                ],
                                "description": "Catalog credential type. Clients map `license` -> LIC badge, `certification` -> CERT badge, `skill` -> no badge.\n",
                                "example": "certification"
                              },
                              "requires_document_upload": {
                                "type": "boolean",
                                "example": false
                              },
                              "active": {
                                "type": "boolean",
                                "example": true
                              },
                              "created_at": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2024-01-15T10:00:00Z"
                              },
                              "updated_at": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2024-01-15T10:00:00Z"
                              }
                            }
                          },
                          {
                            "type": "object",
                            "properties": {
                              "recommendation_score": {
                                "type": "number",
                                "format": "float",
                                "example": 0.85
                              },
                              "recommendation_reason": {
                                "type": "string",
                                "example": "Required for your current role: Software Engineer"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/skills/proficiency_levels": {
      "get": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "Get proficiency levels",
        "description": "Get the fixed 1–5 proficiency scale with this business's configured level names and descriptions (falling back to platform defaults). Use this to populate a proficiency picker without hardcoding labels — the integer `level` is what you submit to the employee_skills endpoints.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Proficiency levels retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "proficiency_levels": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "level": {
                            "type": "integer",
                            "example": 3
                          },
                          "name": {
                            "type": "string",
                            "example": "Intermediate"
                          },
                          "description": {
                            "type": "string",
                            "example": "Solid proficiency in the skill. Can work independently on most tasks."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/employee_skills": {
      "get": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "List employee skills",
        "description": "Get the current user's skills with filtering and sorting options.\n\n**Use Cases:**\n- View employee's skill profile\n- Filter skills by category or status\n- Check certification expiration dates\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "description": "Filter by skill category",
            "schema": {
              "type": "string",
              "example": "technical_skills"
            }
          },
          {
            "name": "proficiency_level",
            "in": "query",
            "description": "Filter by proficiency level",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 5,
              "example": 3
            }
          },
          {
            "name": "verification_status",
            "in": "query",
            "description": "Filter by verification status",
            "schema": {
              "type": "string",
              "enum": [
                "verified",
                "unverified",
                "pending"
              ],
              "example": "verified"
            }
          },
          {
            "name": "certification_status",
            "in": "query",
            "description": "Filter by certification status",
            "schema": {
              "type": "string",
              "enum": [
                "certified",
                "expiring",
                "expired"
              ],
              "example": "certified"
            }
          },
          {
            "name": "expiring_days",
            "in": "query",
            "description": "Days ahead to check for expiring certifications (used with certification_status=expiring)",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "example": 30
            }
          },
          {
            "name": "active",
            "in": "query",
            "description": "Filter by active status",
            "schema": {
              "type": "boolean",
              "example": true
            }
          },
          {
            "name": "sort_by",
            "in": "query",
            "description": "Sort by field",
            "schema": {
              "type": "string",
              "enum": [
                "skill_name",
                "proficiency_level",
                "certification_date",
                "expiration_date",
                "created_at"
              ],
              "example": "skill_name"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "example": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Number of items per page",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "example": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Employee skills retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 456
                          },
                          "skill_id": {
                            "type": "integer",
                            "example": 123
                          },
                          "proficiency_level": {
                            "type": "integer",
                            "minimum": 1,
                            "maximum": 5,
                            "example": 3
                          },
                          "certification_date": {
                            "type": "string",
                            "format": "date",
                            "nullable": true,
                            "example": "2024-01-15"
                          },
                          "expiration_date": {
                            "type": "string",
                            "format": "date",
                            "nullable": true,
                            "example": "2025-01-15"
                          },
                          "certification_number": {
                            "type": "string",
                            "nullable": true,
                            "example": "CERT-2024-001"
                          },
                          "notes": {
                            "type": "string",
                            "nullable": true,
                            "example": "Completed advanced course"
                          },
                          "verified": {
                            "type": "boolean",
                            "example": true
                          },
                          "verified_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "example": "2024-01-16T10:00:00Z"
                          },
                          "active": {
                            "type": "boolean",
                            "example": true
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2024-01-15T10:00:00Z"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2024-01-15T10:00:00Z"
                          },
                          "skill": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 123
                              },
                              "name": {
                                "type": "string",
                                "example": "JavaScript Development"
                              },
                              "category": {
                                "type": "string",
                                "nullable": true,
                                "example": "technical_skills"
                              },
                              "category_display": {
                                "type": "string",
                                "nullable": true,
                                "example": "Technical Skills"
                              },
                              "requires_certification": {
                                "type": "boolean",
                                "example": true
                              },
                              "skill_kind": {
                                "type": "string",
                                "enum": [
                                  "skill",
                                  "certification",
                                  "license"
                                ],
                                "description": "Raw catalog credential type. Drives the LIC/CERT badge in the web/mobile UI — clients map license -> \"LIC\", certification -> \"CERT\", skill -> no badge.",
                                "example": "certification"
                              }
                            }
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 6
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Items per page",
                          "example": 25
                        }
                      },
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "Add employee skill",
        "description": "Add a new skill to the current user's profile.\n\n**Use Cases:**\n- Employee adds a new skill they possess\n- Record certification information\n- Set proficiency level\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "skill_id",
                  "proficiency_level"
                ],
                "properties": {
                  "skill_id": {
                    "type": "integer",
                    "description": "ID of the skill to add",
                    "example": 123
                  },
                  "proficiency_level": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 5,
                    "description": "Proficiency level (1=Basic, 2=Beginner, 3=Intermediate, 4=Advanced, 5=Expert)",
                    "example": 3
                  },
                  "certification_date": {
                    "type": "string",
                    "format": "date",
                    "description": "Date when certification was obtained",
                    "example": "2024-01-15"
                  },
                  "expiration_date": {
                    "type": "string",
                    "format": "date",
                    "description": "Date when certification expires",
                    "example": "2025-01-15"
                  },
                  "certification_number": {
                    "type": "string",
                    "description": "Certification number or ID",
                    "example": "CERT-2024-001"
                  },
                  "notes": {
                    "type": "string",
                    "description": "Additional notes about the skill",
                    "example": "Completed advanced JavaScript course"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Employee skill created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "employee_skill": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/EmployeeSkill"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "verifier": {
                              "type": "object",
                              "nullable": true,
                              "properties": {
                                "id": {
                                  "type": "integer",
                                  "example": 789
                                },
                                "name": {
                                  "type": "string",
                                  "example": "John Manager"
                                }
                              }
                            },
                            "days_until_expiration": {
                              "type": "integer",
                              "nullable": true,
                              "example": 45
                            },
                            "expired": {
                              "type": "boolean",
                              "example": false
                            },
                            "expiring_soon": {
                              "type": "boolean",
                              "example": true
                            },
                            "proficiency_text": {
                              "type": "string",
                              "example": "Intermediate"
                            },
                            "verification_status": {
                              "type": "string",
                              "enum": [
                                "verified",
                                "pending",
                                "unverified"
                              ],
                              "example": "verified"
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          }
        }
      }
    },
    "/employee_skills/{id}": {
      "get": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "Get employee skill details",
        "description": "Get detailed information about a specific employee skill",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Employee skill ID",
            "schema": {
              "type": "integer",
              "example": 456
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Employee skill details retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "employee_skill": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/EmployeeSkill"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "verifier": {
                              "type": "object",
                              "nullable": true,
                              "properties": {
                                "id": {
                                  "type": "integer",
                                  "example": 789
                                },
                                "name": {
                                  "type": "string",
                                  "example": "John Manager"
                                }
                              }
                            },
                            "days_until_expiration": {
                              "type": "integer",
                              "nullable": true,
                              "example": 45
                            },
                            "expired": {
                              "type": "boolean",
                              "example": false
                            },
                            "expiring_soon": {
                              "type": "boolean",
                              "example": true
                            },
                            "proficiency_text": {
                              "type": "string",
                              "example": "Intermediate"
                            },
                            "verification_status": {
                              "type": "string",
                              "enum": [
                                "verified",
                                "pending",
                                "unverified"
                              ],
                              "example": "verified"
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "Update employee skill",
        "description": "Update an existing employee skill. Note that updating verified skills will reset verification status unless force_update=true is used.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Employee skill ID",
            "schema": {
              "type": "integer",
              "example": 456
            }
          },
          {
            "name": "force_update",
            "in": "query",
            "description": "Force update even if skill is verified (resets verification)",
            "schema": {
              "type": "boolean",
              "example": false
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "proficiency_level": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 5,
                    "description": "Proficiency level",
                    "example": 4
                  },
                  "certification_date": {
                    "type": "string",
                    "format": "date",
                    "description": "Date when certification was obtained",
                    "example": "2024-01-15"
                  },
                  "expiration_date": {
                    "type": "string",
                    "format": "date",
                    "description": "Date when certification expires",
                    "example": "2025-01-15"
                  },
                  "certification_number": {
                    "type": "string",
                    "description": "Certification number or ID",
                    "example": "CERT-2024-002"
                  },
                  "notes": {
                    "type": "string",
                    "description": "Additional notes about the skill",
                    "example": "Updated certification information"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Employee skill updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "employee_skill": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/EmployeeSkill"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "verifier": {
                              "type": "object",
                              "nullable": true,
                              "properties": {
                                "id": {
                                  "type": "integer",
                                  "example": 789
                                },
                                "name": {
                                  "type": "string",
                                  "example": "John Manager"
                                }
                              }
                            },
                            "days_until_expiration": {
                              "type": "integer",
                              "nullable": true,
                              "example": 45
                            },
                            "expired": {
                              "type": "boolean",
                              "example": false
                            },
                            "expiring_soon": {
                              "type": "boolean",
                              "example": true
                            },
                            "proficiency_text": {
                              "type": "string",
                              "example": "Intermediate"
                            },
                            "verification_status": {
                              "type": "string",
                              "enum": [
                                "verified",
                                "pending",
                                "unverified"
                              ],
                              "example": "verified"
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "Remove employee skill",
        "description": "Remove a skill from the current user's profile",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Employee skill ID",
            "schema": {
              "type": "integer",
              "example": 456
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Employee skill removed successfully"
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/employee_skills/bulk": {
      "get": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "Get skills for multiple users",
        "description": "Fetch confirmed skills for a set of users, grouped by user.\n\nAny authenticated caller may fetch the requested users' skills; access\nis not restricted by manager/role hierarchy. The only boundary is tenant\nisolation — requested user ids that are not members of the caller's\nbusiness (or do not exist) are omitted from `users` and listed in\n`meta.skipped_user_ids` rather than failing the request. A maximum of\n100 user ids may be requested at once.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "user_ids",
            "in": "query",
            "required": true,
            "description": "User ids to fetch skills for (comma-separated or repeated). Max 100.",
            "style": "form",
            "explode": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "integer"
              },
              "example": [
                12,
                15,
                999
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Skills retrieved successfully for the authorized users",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "users": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "user": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 12
                              },
                              "name": {
                                "type": "string",
                                "example": "Jane Doe"
                              }
                            }
                          },
                          "skills": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "type": "integer",
                                  "example": 456
                                },
                                "skill_id": {
                                  "type": "integer",
                                  "example": 123
                                },
                                "proficiency_level": {
                                  "type": "integer",
                                  "minimum": 1,
                                  "maximum": 5,
                                  "example": 3
                                },
                                "certification_date": {
                                  "type": "string",
                                  "format": "date",
                                  "nullable": true,
                                  "example": "2024-01-15"
                                },
                                "expiration_date": {
                                  "type": "string",
                                  "format": "date",
                                  "nullable": true,
                                  "example": "2025-01-15"
                                },
                                "certification_number": {
                                  "type": "string",
                                  "nullable": true,
                                  "example": "CERT-2024-001"
                                },
                                "notes": {
                                  "type": "string",
                                  "nullable": true,
                                  "example": "Completed advanced course"
                                },
                                "verified": {
                                  "type": "boolean",
                                  "example": true
                                },
                                "verified_at": {
                                  "type": "string",
                                  "format": "date-time",
                                  "nullable": true,
                                  "example": "2024-01-16T10:00:00Z"
                                },
                                "active": {
                                  "type": "boolean",
                                  "example": true
                                },
                                "created_at": {
                                  "type": "string",
                                  "format": "date-time",
                                  "example": "2024-01-15T10:00:00Z"
                                },
                                "updated_at": {
                                  "type": "string",
                                  "format": "date-time",
                                  "example": "2024-01-15T10:00:00Z"
                                },
                                "skill": {
                                  "type": "object",
                                  "properties": {
                                    "id": {
                                      "type": "integer",
                                      "example": 123
                                    },
                                    "name": {
                                      "type": "string",
                                      "example": "JavaScript Development"
                                    },
                                    "category": {
                                      "type": "string",
                                      "nullable": true,
                                      "example": "technical_skills"
                                    },
                                    "category_display": {
                                      "type": "string",
                                      "nullable": true,
                                      "example": "Technical Skills"
                                    },
                                    "requires_certification": {
                                      "type": "boolean",
                                      "example": true
                                    },
                                    "skill_kind": {
                                      "type": "string",
                                      "enum": [
                                        "skill",
                                        "certification",
                                        "license"
                                      ],
                                      "description": "Raw catalog credential type. Drives the LIC/CERT badge in the web/mobile UI — clients map license -> \"LIC\", certification -> \"CERT\", skill -> no badge.",
                                      "example": "certification"
                                    }
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "requested_count": {
                          "type": "integer",
                          "example": 3
                        },
                        "returned_count": {
                          "type": "integer",
                          "example": 2
                        },
                        "skill_row_count": {
                          "type": "integer",
                          "description": "Skill rows returned across all requested users.",
                          "example": 120
                        },
                        "skill_row_limit": {
                          "type": "integer",
                          "description": "Maximum skill rows this endpoint returns in one response.",
                          "example": 2500
                        },
                        "truncated": {
                          "type": "boolean",
                          "description": "True when `skill_row_limit` was hit and later rows were dropped. Which rows survive follows the request's sort order.",
                          "example": false
                        },
                        "skipped_user_ids": {
                          "type": "array",
                          "items": {
                            "type": "integer"
                          },
                          "example": [
                            999
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/employee_skills/expiring": {
      "get": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "Get expiring skills",
        "description": "Get employee skills with certifications that are expiring soon",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "description": "Number of days ahead to check for expiring certifications",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "example": 90
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Expiring skills retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "expiring_skills": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 456
                              },
                              "skill_id": {
                                "type": "integer",
                                "example": 123
                              },
                              "proficiency_level": {
                                "type": "integer",
                                "minimum": 1,
                                "maximum": 5,
                                "example": 3
                              },
                              "certification_date": {
                                "type": "string",
                                "format": "date",
                                "nullable": true,
                                "example": "2024-01-15"
                              },
                              "expiration_date": {
                                "type": "string",
                                "format": "date",
                                "nullable": true,
                                "example": "2025-01-15"
                              },
                              "certification_number": {
                                "type": "string",
                                "nullable": true,
                                "example": "CERT-2024-001"
                              },
                              "notes": {
                                "type": "string",
                                "nullable": true,
                                "example": "Completed advanced course"
                              },
                              "verified": {
                                "type": "boolean",
                                "example": true
                              },
                              "verified_at": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true,
                                "example": "2024-01-16T10:00:00Z"
                              },
                              "active": {
                                "type": "boolean",
                                "example": true
                              },
                              "created_at": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2024-01-15T10:00:00Z"
                              },
                              "updated_at": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2024-01-15T10:00:00Z"
                              },
                              "skill": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 123
                                  },
                                  "name": {
                                    "type": "string",
                                    "example": "JavaScript Development"
                                  },
                                  "category": {
                                    "type": "string",
                                    "nullable": true,
                                    "example": "technical_skills"
                                  },
                                  "category_display": {
                                    "type": "string",
                                    "nullable": true,
                                    "example": "Technical Skills"
                                  },
                                  "requires_certification": {
                                    "type": "boolean",
                                    "example": true
                                  },
                                  "skill_kind": {
                                    "type": "string",
                                    "enum": [
                                      "skill",
                                      "certification",
                                      "license"
                                    ],
                                    "description": "Raw catalog credential type. Drives the LIC/CERT badge in the web/mobile UI — clients map license -> \"LIC\", certification -> \"CERT\", skill -> no badge.",
                                    "example": "certification"
                                  }
                                }
                              }
                            }
                          },
                          {
                            "type": "object",
                            "properties": {
                              "days_until_expiration": {
                                "type": "integer",
                                "example": 15
                              },
                              "expiration_urgency": {
                                "type": "string",
                                "enum": [
                                  "expired",
                                  "critical",
                                  "warning",
                                  "notice",
                                  "normal"
                                ],
                                "example": "warning"
                              }
                            }
                          }
                        ]
                      }
                    },
                    "total_count": {
                      "type": "integer",
                      "example": 3
                    },
                    "days_ahead": {
                      "type": "integer",
                      "example": 90
                    },
                    "truncated": {
                      "type": "boolean",
                      "description": "True when the result set hit `row_limit` and was cut. When true, `total_count` counts the RETURNED rows, not the whole matching set, and any summary/aggregate in this response is likewise page-local.",
                      "example": false
                    },
                    "row_limit": {
                      "type": "integer",
                      "description": "Maximum rows this endpoint will return in one response.",
                      "example": 500
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/employee_skills/categories": {
      "get": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "Get employee skills by category",
        "description": "Get employee's skills grouped by category with statistics",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Skills categories retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "categories": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "category": {
                            "type": "string",
                            "example": "technical_skills"
                          },
                          "category_display": {
                            "type": "string",
                            "example": "Technical Skills"
                          },
                          "skill_count": {
                            "type": "integer",
                            "example": 5
                          },
                          "average_proficiency": {
                            "type": "number",
                            "format": "float",
                            "example": 3.4
                          },
                          "certified_count": {
                            "type": "integer",
                            "example": 2
                          },
                          "expiring_soon_count": {
                            "type": "integer",
                            "example": 1
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/employee_skills/{id}/request_verification": {
      "post": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "Request skill verification",
        "description": "Request verification for an employee skill from manager",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Employee skill ID",
            "schema": {
              "type": "integer",
              "example": 456
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Verification request sent successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Verification request sent to your manager"
                    },
                    "employee_skill": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 456
                        },
                        "skill_id": {
                          "type": "integer",
                          "example": 123
                        },
                        "proficiency_level": {
                          "type": "integer",
                          "minimum": 1,
                          "maximum": 5,
                          "example": 3
                        },
                        "certification_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "example": "2024-01-15"
                        },
                        "expiration_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "example": "2025-01-15"
                        },
                        "certification_number": {
                          "type": "string",
                          "nullable": true,
                          "example": "CERT-2024-001"
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true,
                          "example": "Completed advanced course"
                        },
                        "verified": {
                          "type": "boolean",
                          "example": true
                        },
                        "verified_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "example": "2024-01-16T10:00:00Z"
                        },
                        "active": {
                          "type": "boolean",
                          "example": true
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2024-01-15T10:00:00Z"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2024-01-15T10:00:00Z"
                        },
                        "skill": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 123
                            },
                            "name": {
                              "type": "string",
                              "example": "JavaScript Development"
                            },
                            "category": {
                              "type": "string",
                              "nullable": true,
                              "example": "technical_skills"
                            },
                            "category_display": {
                              "type": "string",
                              "nullable": true,
                              "example": "Technical Skills"
                            },
                            "requires_certification": {
                              "type": "boolean",
                              "example": true
                            },
                            "skill_kind": {
                              "type": "string",
                              "enum": [
                                "skill",
                                "certification",
                                "license"
                              ],
                              "description": "Raw catalog credential type. Drives the LIC/CERT badge in the web/mobile UI — clients map license -> \"LIC\", certification -> \"CERT\", skill -> no badge.",
                              "example": "certification"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          }
        }
      }
    },
    "/certifications": {
      "get": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "List employee certifications",
        "description": "Get the current user's certifications with filtering and sorting options.\n\n**Use Cases:**\n- View all employee certifications\n- Check certification status and expiration dates\n- Filter by category or verification status\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "description": "Filter by skill category",
            "schema": {
              "type": "string",
              "example": "technical_skills"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by certification status",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "expired",
                "expiring"
              ],
              "example": "active"
            }
          },
          {
            "name": "expiring_days",
            "in": "query",
            "description": "Days ahead to check for expiring certifications (used with status=expiring)",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "example": 30
            }
          },
          {
            "name": "verification_status",
            "in": "query",
            "description": "Filter by verification status",
            "schema": {
              "type": "string",
              "enum": [
                "verified",
                "unverified"
              ],
              "example": "verified"
            }
          },
          {
            "name": "sort_by",
            "in": "query",
            "description": "Sort by field",
            "schema": {
              "type": "string",
              "enum": [
                "skill_name",
                "certification_date",
                "expiration_date",
                "created_at"
              ],
              "example": "expiration_date"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "example": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Number of items per page",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "example": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Certifications retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 456
                          },
                          "skill_id": {
                            "type": "integer",
                            "example": 123
                          },
                          "certification_date": {
                            "type": "string",
                            "format": "date",
                            "example": "2024-01-15"
                          },
                          "expiration_date": {
                            "type": "string",
                            "format": "date",
                            "nullable": true,
                            "example": "2025-01-15"
                          },
                          "certification_number": {
                            "type": "string",
                            "nullable": true,
                            "example": "CERT-2024-001"
                          },
                          "verified": {
                            "type": "boolean",
                            "example": true
                          },
                          "verified_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "example": "2024-01-16T10:00:00Z"
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2024-01-15T10:00:00Z"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2024-01-15T10:00:00Z"
                          },
                          "skill": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 123
                              },
                              "name": {
                                "type": "string",
                                "example": "JavaScript Development"
                              },
                              "category": {
                                "type": "string",
                                "nullable": true,
                                "example": "technical_skills"
                              },
                              "category_display": {
                                "type": "string",
                                "nullable": true,
                                "example": "Technical Skills"
                              }
                            }
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 6
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Items per page",
                          "example": 25
                        }
                      },
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/certifications/{id}": {
      "get": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "Get certification details",
        "description": "Get detailed information about a specific certification",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Certification ID (employee skill ID)",
            "schema": {
              "type": "integer",
              "example": 456
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Certification details retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "certification": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Certification"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "notes": {
                              "type": "string",
                              "nullable": true,
                              "example": "Completed advanced course"
                            },
                            "proficiency_level": {
                              "type": "integer",
                              "minimum": 1,
                              "maximum": 5,
                              "example": 3
                            },
                            "verifier": {
                              "type": "object",
                              "nullable": true,
                              "properties": {
                                "id": {
                                  "type": "integer",
                                  "example": 789
                                },
                                "name": {
                                  "type": "string",
                                  "example": "John Manager"
                                }
                              }
                            },
                            "days_until_expiration": {
                              "type": "integer",
                              "nullable": true,
                              "example": 45
                            },
                            "expired": {
                              "type": "boolean",
                              "example": false
                            },
                            "expiring_soon": {
                              "type": "boolean",
                              "example": true
                            },
                            "status": {
                              "type": "string",
                              "enum": [
                                "active",
                                "expiring_soon",
                                "expiring_notice",
                                "expired"
                              ],
                              "example": "active"
                            },
                            "document_attached": {
                              "type": "boolean",
                              "example": true
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/certifications/expiring": {
      "get": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "Get expiring certifications",
        "description": "Get certifications that are expiring soon with urgency levels",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "description": "Number of days ahead to check for expiring certifications",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "example": 90
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Expiring certifications retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "expiring_certifications": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 456
                              },
                              "skill_id": {
                                "type": "integer",
                                "example": 123
                              },
                              "certification_date": {
                                "type": "string",
                                "format": "date",
                                "example": "2024-01-15"
                              },
                              "expiration_date": {
                                "type": "string",
                                "format": "date",
                                "nullable": true,
                                "example": "2025-01-15"
                              },
                              "certification_number": {
                                "type": "string",
                                "nullable": true,
                                "example": "CERT-2024-001"
                              },
                              "verified": {
                                "type": "boolean",
                                "example": true
                              },
                              "verified_at": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true,
                                "example": "2024-01-16T10:00:00Z"
                              },
                              "created_at": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2024-01-15T10:00:00Z"
                              },
                              "updated_at": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2024-01-15T10:00:00Z"
                              },
                              "skill": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 123
                                  },
                                  "name": {
                                    "type": "string",
                                    "example": "JavaScript Development"
                                  },
                                  "category": {
                                    "type": "string",
                                    "nullable": true,
                                    "example": "technical_skills"
                                  },
                                  "category_display": {
                                    "type": "string",
                                    "nullable": true,
                                    "example": "Technical Skills"
                                  }
                                }
                              }
                            }
                          },
                          {
                            "type": "object",
                            "properties": {
                              "days_until_expiration": {
                                "type": "integer",
                                "example": 15
                              },
                              "expiration_urgency": {
                                "type": "string",
                                "enum": [
                                  "expired",
                                  "critical",
                                  "warning",
                                  "notice",
                                  "normal"
                                ],
                                "example": "warning"
                              },
                              "renewal_required": {
                                "type": "boolean",
                                "example": true
                              }
                            }
                          }
                        ]
                      }
                    },
                    "total_count": {
                      "type": "integer",
                      "example": 3
                    },
                    "days_ahead": {
                      "type": "integer",
                      "example": 90
                    },
                    "truncated": {
                      "type": "boolean",
                      "description": "True when the result set hit `row_limit` and was cut. When true, `total_count` counts the RETURNED rows, not the whole matching set, and any summary/aggregate in this response is likewise page-local.",
                      "example": false
                    },
                    "row_limit": {
                      "type": "integer",
                      "description": "Maximum rows this endpoint will return in one response.",
                      "example": 500
                    },
                    "summary": {
                      "type": "object",
                      "properties": {
                        "critical": {
                          "type": "integer",
                          "example": 1
                        },
                        "warning": {
                          "type": "integer",
                          "example": 2
                        },
                        "notice": {
                          "type": "integer",
                          "example": 0
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/certifications/expired": {
      "get": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "Get expired certifications",
        "description": "Get certifications that have already expired",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Expired certifications retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "expired_certifications": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 456
                              },
                              "skill_id": {
                                "type": "integer",
                                "example": 123
                              },
                              "certification_date": {
                                "type": "string",
                                "format": "date",
                                "example": "2024-01-15"
                              },
                              "expiration_date": {
                                "type": "string",
                                "format": "date",
                                "nullable": true,
                                "example": "2025-01-15"
                              },
                              "certification_number": {
                                "type": "string",
                                "nullable": true,
                                "example": "CERT-2024-001"
                              },
                              "verified": {
                                "type": "boolean",
                                "example": true
                              },
                              "verified_at": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true,
                                "example": "2024-01-16T10:00:00Z"
                              },
                              "created_at": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2024-01-15T10:00:00Z"
                              },
                              "updated_at": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2024-01-15T10:00:00Z"
                              },
                              "skill": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 123
                                  },
                                  "name": {
                                    "type": "string",
                                    "example": "JavaScript Development"
                                  },
                                  "category": {
                                    "type": "string",
                                    "nullable": true,
                                    "example": "technical_skills"
                                  },
                                  "category_display": {
                                    "type": "string",
                                    "nullable": true,
                                    "example": "Technical Skills"
                                  }
                                }
                              }
                            }
                          },
                          {
                            "type": "object",
                            "properties": {
                              "days_expired": {
                                "type": "integer",
                                "example": 30
                              },
                              "renewal_required": {
                                "type": "boolean",
                                "example": true
                              }
                            }
                          }
                        ]
                      }
                    },
                    "total_count": {
                      "type": "integer",
                      "example": 2
                    },
                    "truncated": {
                      "type": "boolean",
                      "description": "True when the result set hit `row_limit` and was cut. When true, `total_count` counts the RETURNED rows, not the whole matching set.",
                      "example": false
                    },
                    "row_limit": {
                      "type": "integer",
                      "description": "Maximum rows this endpoint will return in one response.",
                      "example": 500
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/certifications/summary": {
      "get": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "Get certification summary",
        "description": "Get summary statistics of employee's certifications",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Certification summary retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "certification_summary": {
                      "type": "object",
                      "properties": {
                        "total_certifications": {
                          "type": "integer",
                          "example": 8
                        },
                        "active_certifications": {
                          "type": "integer",
                          "example": 6
                        },
                        "expired_certifications": {
                          "type": "integer",
                          "example": 2
                        },
                        "expiring_soon": {
                          "type": "integer",
                          "example": 1
                        },
                        "verified_certifications": {
                          "type": "integer",
                          "example": 7
                        },
                        "by_category": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "object",
                            "properties": {
                              "total": {
                                "type": "integer"
                              },
                              "active": {
                                "type": "integer"
                              },
                              "expiring_soon": {
                                "type": "integer"
                              }
                            }
                          }
                        },
                        "recent_activity": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "skill_name": {
                                "type": "string",
                                "example": "JavaScript Development"
                              },
                              "certification_date": {
                                "type": "string",
                                "format": "date",
                                "example": "2024-01-15"
                              },
                              "created_at": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2024-01-15T10:00:00Z"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/certifications/{id}/renew": {
      "post": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "Renew certification",
        "description": "Renew an existing certification with new dates and information.\n\n**Use Cases:**\n- Employee renews an expiring certification\n- Update certification with new expiration date\n- Record new certification number\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Certification ID (employee skill ID)",
            "schema": {
              "type": "integer",
              "example": 456
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "new_certification_date": {
                    "type": "string",
                    "format": "date",
                    "description": "New certification date",
                    "example": "2024-02-01"
                  },
                  "new_expiration_date": {
                    "type": "string",
                    "format": "date",
                    "description": "New expiration date",
                    "example": "2025-02-01"
                  },
                  "new_certification_number": {
                    "type": "string",
                    "description": "New certification number",
                    "example": "CERT-2024-003"
                  },
                  "notes": {
                    "type": "string",
                    "description": "Notes about the renewal",
                    "example": "Renewed with updated curriculum"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Certification renewed successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Certification renewed successfully"
                    },
                    "certification": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Certification"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "notes": {
                              "type": "string",
                              "nullable": true,
                              "example": "Completed advanced course"
                            },
                            "proficiency_level": {
                              "type": "integer",
                              "minimum": 1,
                              "maximum": 5,
                              "example": 3
                            },
                            "verifier": {
                              "type": "object",
                              "nullable": true,
                              "properties": {
                                "id": {
                                  "type": "integer",
                                  "example": 789
                                },
                                "name": {
                                  "type": "string",
                                  "example": "John Manager"
                                }
                              }
                            },
                            "days_until_expiration": {
                              "type": "integer",
                              "nullable": true,
                              "example": 45
                            },
                            "expired": {
                              "type": "boolean",
                              "example": false
                            },
                            "expiring_soon": {
                              "type": "boolean",
                              "example": true
                            },
                            "status": {
                              "type": "string",
                              "enum": [
                                "active",
                                "expiring_soon",
                                "expiring_notice",
                                "expired"
                              ],
                              "example": "active"
                            },
                            "document_attached": {
                              "type": "boolean",
                              "example": true
                            }
                          }
                        }
                      ]
                    },
                    "renewal_id": {
                      "type": "string",
                      "example": "renewal-uuid-123"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          }
        }
      }
    },
    "/certifications/upload_document": {
      "post": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "Upload certification document",
        "description": "Upload a document file for a certification",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file",
                  "employee_skill_id"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "Certification document file"
                  },
                  "employee_skill_id": {
                    "type": "integer",
                    "description": "Employee skill ID for the certification",
                    "example": 456
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Document uploaded successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Certification document uploaded successfully"
                    },
                    "document": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 789
                        },
                        "filename": {
                          "type": "string",
                          "example": "certificate.pdf"
                        },
                        "content_type": {
                          "type": "string",
                          "example": "application/pdf"
                        },
                        "byte_size": {
                          "type": "integer",
                          "example": 1024000
                        },
                        "url": {
                          "type": "string",
                          "example": "/rails/active_storage/blobs/xyz/certificate.pdf"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          }
        }
      }
    },
    "/safety_hub/dashboard": {
      "get": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Dashboard KPI tiles",
        "description": "Returns the five home-screen KPI tiles in display order. Each tile is\nself-describing — it carries its own label, count, and Bootstrap text\ncolor class so native-mobile clients render the same palette as the\nweb Safety Hub dashboard.\n\nTiles, in order:\n  1. `days_without_injury` — days since the most recent injury-type\n     incident (0 if none on record). Color: `text-success`.\n  2. `certifications_expiring_soon` — count of certifications expiring\n     within the business-configured reminder window (default 30 days),\n     aggregated across EmployeeSkill, TrainingCertificate, and\n     LmsTrainingRecord. Color: `text-warning`.\n  3. `observations_this_month` — safety observations recorded since\n     the start of the current month. Color: `text-primary`.\n  4. `incidents_this_month` — non-cancelled incidents occurring since\n     the start of the current month. Color: `text-danger`.\n  5. `upcoming_toolbox_talks` — toolbox talks scheduled in the future.\n     Color: `text-info`.\n",
        "responses": {
          "200": {
            "description": "Dashboard KPI tiles",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "tiles"
                  ],
                  "properties": {
                    "tiles": {
                      "type": "array",
                      "description": "KPI tiles in display order. Always 5 items.",
                      "minItems": 5,
                      "maxItems": 5,
                      "items": {
                        "type": "object",
                        "required": [
                          "key",
                          "label",
                          "count",
                          "color_class"
                        ],
                        "properties": {
                          "key": {
                            "type": "string",
                            "enum": [
                              "days_without_injury",
                              "certifications_expiring_soon",
                              "observations_this_month",
                              "incidents_this_month",
                              "upcoming_toolbox_talks"
                            ],
                            "description": "Stable machine key identifying the tile."
                          },
                          "label": {
                            "type": "string",
                            "description": "Human-readable title shown on the tile.",
                            "example": "Days Without Injury"
                          },
                          "count": {
                            "type": "integer",
                            "minimum": 0,
                            "description": "Numeric value for the tile.",
                            "example": 42
                          },
                          "color_class": {
                            "type": "string",
                            "enum": [
                              "text-success",
                              "text-warning",
                              "text-primary",
                              "text-danger",
                              "text-info"
                            ],
                            "description": "Bootstrap text color class to apply to the tile value."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — bearer token missing or invalid"
          },
          "403": {
            "description": "Forbidden — Safety Hub not enabled for the business",
            "or user lacks access": null
          }
        }
      }
    },
    "/safety_hub/config": {
      "get": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Create-form picker vocabularies",
        "description": "The option lists a native client needs to render the Safety Hub create\nforms — the API twin of the selects the desktop `new` views build inline.\n\nEach block is present ONLY when its module is enabled for the business,\nmirroring every other endpoint here (a tenant with Permits switched off\ngets no `permit_types` / `contractors`). Keys are stable, so a client\nrenders whatever blocks are present:\n\n  * `incident_types`, `incident_severities` — the two required selects on\n    the report-incident form. Sourced from the Incident model's own enums,\n    so they can never drift from what `POST /safety_hub/incidents` accepts.\n  * `observation_categories` — the required Category select on the\n    observation form. This is the TENANT'S configured list (Safety Hub\n    app setting), so it varies per business; values are display-ready\n    strings (`label` == `value`).\n  * `permit_types` — the Permit Type select, from the WorkPermit type\n    catalog; each carries a human `label` and a Font Awesome `icon`.\n  * `contractors` — the tenant's active vendor roster for the\n    \"Contractor (if external)\" select; the permit form writes the chosen\n    `id` to `vendor_id`.\n\nRead-only. Requires `read:safety_hub`. No pagination and no role branch —\nthese are business-wide form vocabularies, identical for every member.\n",
        "responses": {
          "200": {
            "description": "The enabled modules' picker vocabularies.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "config"
                  ],
                  "properties": {
                    "config": {
                      "type": "object",
                      "description": "Only the enabled modules' blocks are present. Absent keys\nmean that module is off for the business.\n",
                      "properties": {
                        "incident_types": {
                          "type": "array",
                          "description": "Incident Type select options (Incidents module).",
                          "items": {
                            "type": "object",
                            "required": [
                              "value",
                              "label"
                            ],
                            "properties": {
                              "value": {
                                "type": "string",
                                "example": "near_miss"
                              },
                              "label": {
                                "type": "string",
                                "example": "Near Miss"
                              }
                            }
                          }
                        },
                        "incident_severities": {
                          "type": "array",
                          "description": "Severity select options (Incidents module).",
                          "items": {
                            "type": "object",
                            "required": [
                              "value",
                              "label"
                            ],
                            "properties": {
                              "value": {
                                "type": "string",
                                "example": "high"
                              },
                              "label": {
                                "type": "string",
                                "example": "High"
                              }
                            }
                          }
                        },
                        "observation_categories": {
                          "type": "array",
                          "description": "Observation Category select options (Observations\nmodule). The tenant's configured list — label == value.\n",
                          "items": {
                            "type": "object",
                            "required": [
                              "value",
                              "label"
                            ],
                            "properties": {
                              "value": {
                                "type": "string",
                                "example": "PPE"
                              },
                              "label": {
                                "type": "string",
                                "example": "PPE"
                              }
                            }
                          }
                        },
                        "permit_types": {
                          "type": "array",
                          "description": "Permit Type select options (Permits module).",
                          "items": {
                            "type": "object",
                            "required": [
                              "value",
                              "label",
                              "icon"
                            ],
                            "properties": {
                              "value": {
                                "type": "string",
                                "example": "hot_work"
                              },
                              "label": {
                                "type": "string",
                                "example": "Hot work"
                              },
                              "icon": {
                                "type": "string",
                                "example": "fa-fire"
                              }
                            }
                          }
                        },
                        "contractors": {
                          "type": "array",
                          "description": "Active vendors for the \"Contractor (if external)\"\nselect (Permits module). The permit form writes the\nchosen `id` to `vendor_id`.\n",
                          "items": {
                            "type": "object",
                            "required": [
                              "id",
                              "name"
                            ],
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 42
                              },
                              "name": {
                                "type": "string",
                                "example": "Acme Scaffolding Ltd"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — bearer token missing or invalid"
          },
          "403": {
            "description": "Forbidden — Safety Hub not enabled for the business",
            "or user lacks access": null
          }
        }
      }
    },
    "/safety_hub/incidents": {
      "get": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List incidents",
        "description": "Personal feed by DEFAULT: incidents the caller reported or is assigned\nto investigate, newest first. Pass `team=true` for the business-wide\nfeed; that requires Safety Hub manager access and is additionally\nsite-scoped, so a manager restricted to particular locations sees only\nincidents at those sites.\n\nAnonymous incidents are included; filter on the client if needed.\n",
        "parameters": [
          {
            "name": "team",
            "in": "query",
            "description": "Set to `true` for the business-wide feed (manager-gated and\nsite-scoped). Omit or set to `false` for the personal feed.\n",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by lifecycle status",
            "schema": {
              "type": "string",
              "enum": [
                "reported",
                "investigating",
                "investigation_completed",
                "closed",
                "cancelled"
              ]
            }
          },
          {
            "name": "severity",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "low",
                "medium",
                "high",
                "critical"
              ]
            }
          },
          {
            "name": "type",
            "in": "query",
            "description": "Filter by incident_type",
            "schema": {
              "type": "string",
              "enum": [
                "injury",
                "near_miss",
                "property_damage",
                "environmental",
                "security",
                "vehicle_accident",
                "equipment_failure"
              ]
            }
          },
          {
            "name": "location_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "List of incidents",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "incidents": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Incident"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/SafetyHubPaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          }
        }
      },
      "post": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Report a new incident",
        "description": "Report a new incident — the native mirror of the mockup's \"My\nSubmission\" → camera → \"Continue without photo\" → incident form flow,\nand the API twin of the desktop\n`Apps::SafetyHub::IncidentsController#create` / mobile `#create_incident`.\n\n**Any member may file a report** — the caller is always recorded as the\nreporter (and `created_by`) and the status starts `reported`. Only\n`title`, `description`, `incident_type`, `severity` and `occurred_at` are\nrequired; everything else is optional, so a phone can post the bare form\nthe mockup shows and add photos, people and witnesses later.\n\n**Photos** are optional and attach through `MediaItem` (multipart\n`photos[]`), exactly as both web surfaces do, so the AI-vision /\nEXIF-strip / transcription pipelines fire and the detail screen's\ntimeline picks them up. When the tenant has `require_photos_for_injuries`\non, an **injury** report with no photo is refused (422).\n\n`location_id` / `alert_id` that don't belong to the caller's business are\ndropped (never 422'd). Manager notifications and investigator\nauto-assignment are the model's job and fire here identically to every\nother create path.\n\nPost-commit steps (photos, people, witnesses, WCB reportability) that\nfail **after** the incident row is written are collected into `warnings`\nrather than turning a filed report into an error — the report itself is\nstill saved and returned.\n\nRequires the **write** scope: `write:safety_hub`, or `write:own_safety_hub`\n(the by-hand employee grant). Gated on the tenant's `incidents_enabled`\nmodule toggle.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SafetyHubIncidentCreateRequest"
              }
            },
            "multipart/form-data": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SafetyHubIncidentCreateRequest"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "photos": {
                        "type": "array",
                        "description": "Optional scene photos/videos, attached as MediaItems.",
                        "items": {
                          "type": "string",
                          "format": "binary"
                        }
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Incident reported",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "incident": {
                      "$ref": "#/components/schemas/IncidentDetail"
                    },
                    "permissions": {
                      "$ref": "#/components/schemas/SafetyHubIncidentPermissions"
                    },
                    "warnings": {
                      "type": "array",
                      "description": "Post-commit issues that did not block the report (a photo\nthat failed to attach, a person/witness row that failed\nvalidation, WCB reportability that could not be\ndetermined). Empty on a clean save.\n",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request — the incident payload is missing entirely"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — the token lacks the write scope, or Safety Hub / the Incidents module is disabled"
          },
          "422": {
            "description": "Unprocessable — validation failed, or an injury report is missing a required photo"
          }
        }
      }
    },
    "/safety_hub/incidents/{id}": {
      "get": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get one incident",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Incident detail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "incident": {
                      "$ref": "#/components/schemas/IncidentDetail"
                    },
                    "permissions": {
                      "$ref": "#/components/schemas/SafetyHubIncidentPermissions"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Incident not found"
          }
        }
      },
      "patch": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Edit an incident",
        "description": "Correct ONE incident from the native detail \"⋯ → Edit incident\" flow,\nwhich reopens the report form prefilled with the record's own title,\ndescription, immediate actions, type, severity and location. The API twin\nof the desktop `Apps::SafetyHub::IncidentsController#update` (mobile-web\nhas no edit route, so the desktop controller is the authority).\n\nAuthority mirrors the desktop `#authorize_incident_edit` and the detail\nendpoint's `permissions.can_edit`: the incident's **reporter**, OR a\n**safety-hub manager** whose accessible sites include the incident's site.\nA **closed / cancelled** record is frozen (audit-trail integrity) and the\nOSHA/WCB matrix narrows what a late-stage record accepts — both return 422.\n\nThe permitted fields are the SAME set as create; **`status` is not\neditable here** (lifecycle moves through the dedicated workflow actions),\nand photos/people/witnesses are attached **additively** — a\n`require_photos_for_injuries` policy is not re-checked on edit, matching\nthe web. A cross-tenant `location_id` / `alert_id` is dropped rather than\nrejected. Audit rows (`IncidentUpdate`) and platform/WCB side effects are\nthe model's job and fire automatically.\n\nRequires the **write** scope: `write:safety_hub`, or `write:own_safety_hub`\nwhen editing your OWN report. Editing another user's incident via manager\nprivilege requires the wide `write:safety_hub`. Gated on the tenant's\n`incidents_enabled` module toggle.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "incident"
                ],
                "properties": {
                  "incident": {
                    "type": "object",
                    "description": "Any subset of the editable fields. Fields omitted are left\nunchanged.\n",
                    "properties": {
                      "title": {
                        "type": "string",
                        "minLength": 3,
                        "maxLength": 255
                      },
                      "description": {
                        "type": "string",
                        "minLength": 10,
                        "maxLength": 5000
                      },
                      "occurred_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "location_id": {
                        "type": "integer",
                        "description": "A site in the caller's business; a cross-tenant id is dropped."
                      },
                      "incident_type": {
                        "type": "string",
                        "enum": [
                          "injury",
                          "near_miss",
                          "property_damage",
                          "environmental",
                          "security",
                          "vehicle_accident",
                          "equipment_failure"
                        ]
                      },
                      "severity": {
                        "type": "string",
                        "enum": [
                          "low",
                          "medium",
                          "high",
                          "critical"
                        ]
                      },
                      "immediate_actions": {
                        "type": "string"
                      },
                      "anonymous": {
                        "type": "boolean"
                      },
                      "confidential": {
                        "type": "boolean"
                      },
                      "days_away_from_work": {
                        "type": "integer"
                      },
                      "days_restricted_work": {
                        "type": "integer"
                      },
                      "fatality": {
                        "type": "boolean"
                      },
                      "wcb_province": {
                        "type": "string"
                      },
                      "alert_id": {
                        "type": "integer"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Incident updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "incident": {
                      "$ref": "#/components/schemas/IncidentDetail"
                    },
                    "permissions": {
                      "$ref": "#/components/schemas/SafetyHubIncidentPermissions"
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Non-fatal post-save issues (e.g. a photo that failed to attach); the edit still succeeded."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request — the incident payload is missing"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — not the reporter or a site manager, an own-scoped token editing another user's incident, or Safety Hub / the Incidents module disabled"
          },
          "404": {
            "description": "Incident not found"
          },
          "422": {
            "description": "Unprocessable — validation failed, or the incident is closed/cancelled or otherwise frozen by the OSHA/WCB status matrix"
          }
        }
      }
    },
    "/safety_hub/incidents/{id}/investigators": {
      "get": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List an incident's candidate investigators",
        "description": "The candidate investigator list for ONE incident — the native mirror of\nthe desktop show view's \"Reassign Investigator\" picker\n(`Apps::SafetyHub::IncidentsController#load_form_data`'s\n`@potential_investigators`), so a phone can render the same select before\nPOSTing an assignment.\n\nThe candidates are the SHARED `#potential_investigators` — active\n**super_admins / admins / managers** in the business, ordered by first\nname — the SAME relation the desktop picker and its server-side resolve\nuse, so no surface can offer someone the others would not. One query for\nthe whole list; each entry carries `is_current` (whether they are the\nincident's current assignee).\n\n**Manager-only, within accessible sites** — exactly the personas the web\noffers the control to (the Investigation Workflow panel is gated on\n`safety_hub_manager? && incident_within_accessible_sites?`, and the\nreassignment write it feeds carries `authorize_safety_hub_manager!` +\n`authorize_incident_site_access`). A plain member — even the reporter, who\ncan read the incident itself — is refused. `reassignable` echoes the web's\nstatus gate (the picker renders only while the incident is `reported` or\n`investigating`); the list is still returned for a closed incident so a\nmanager can see who would be eligible.\n\nRequires the **read** scope `read:safety_hub` (the manager roster is\nmanagement data, so an own-scoped `read:own_safety_hub` token is refused).\nGated on the tenant's `incidents_enabled` module toggle.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The candidate investigator list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "investigators": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string"
                          },
                          "is_current": {
                            "type": "boolean",
                            "description": "True for the incident's current assigned investigator."
                          }
                        }
                      }
                    },
                    "current_investigator": {
                      "type": "object",
                      "nullable": true,
                      "description": "The currently assigned investigator, or null when unassigned.",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string"
                        }
                      }
                    },
                    "reassignable": {
                      "type": "boolean",
                      "description": "True while the incident is reported or investigating (a closed/cancelled incident's investigator is frozen)."
                    },
                    "incident": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "reference": {
                          "type": "string",
                          "description": "INC-<id>"
                        },
                        "status": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — not a manager, the incident is outside the caller's sites, an own-scoped token, or Safety Hub / the Incidents module disabled"
          },
          "404": {
            "description": "Incident not found (or not in the caller's business)"
          }
        }
      }
    },
    "/safety_hub/incidents/{id}/investigator": {
      "patch": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Assign or reassign an incident's investigator",
        "description": "Assign or reassign ONE incident's investigator — the native mirror of the\ndesktop show view's manager-only \"Reassign Investigator\" picker\n(`Apps::SafetyHub::IncidentsController#assign_investigator`) and the write\nthe candidate list (`GET .../investigators`) feeds.\n\nThe target must be an active **super_admin / admin / manager** of the\nbusiness — the SHARED `#potential_investigators` relation the picker, its\ndesktop resolve and the candidate list all use — so this endpoint can never\naccept someone the select never offered. A missing `investigator_id` is a\n400; an id outside that eligible set is a 422.\n\n**Manager-only, within accessible sites** — exactly the personas the web\noffers the control to (`authorize_safety_hub_manager!` +\n`authorize_incident_site_access`). A closed / cancelled incident's\ninvestigator is frozen (the web renders the picker only while the incident\nis `reported` or `investigating`), returned here as a 422 state conflict.\n\nRoutes through the shared `Incident#reassign_investigator!` — the SAME\ncanonical door the desktop and mobile controllers use: it preserves the\ninvestigation start (a reassignment does not restart the clock), recomputes\nthe deadline, seeds the investigation row only when none exists yet, and\nnotifies the new investigator. The response is the full incident detail\nenvelope, re-read so it reflects the fresh assignment.\n\nRequires the **write** scope `write:safety_hub` (assigning an investigator\nis a management action, so the wide scope is required; a narrow\n`write:own_safety_hub` token is refused). Gated on the tenant's\n`incidents_enabled` module toggle.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "investigator_id"
                ],
                "properties": {
                  "investigator_id": {
                    "type": "integer",
                    "description": "Id of the active safety admin/manager to assign as investigator."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Investigator assigned; the incident detail envelope is returned.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "incident": {
                      "$ref": "#/components/schemas/IncidentDetail"
                    },
                    "permissions": {
                      "$ref": "#/components/schemas/SafetyHubIncidentPermissions"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request — investigator_id missing"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — not a manager, the incident is outside the caller's sites, an own-scoped token, or Safety Hub / the Incidents module disabled"
          },
          "404": {
            "description": "Incident not found (or not in the caller's business)"
          },
          "422": {
            "description": "Unprocessable — the incident is closed/cancelled (its investigator is frozen), or investigator_id is not an active admin/manager of this business"
          }
        }
      },
      "put": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Assign or reassign an incident's investigator (PUT alias)",
        "description": "PUT alias of the PATCH above — identical behaviour, for native clients that\nprefer PUT for an idempotent single-field assignment.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "investigator_id"
                ],
                "properties": {
                  "investigator_id": {
                    "type": "integer",
                    "description": "Id of the active safety admin/manager to assign as investigator."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Investigator assigned; the incident detail envelope is returned.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "incident": {
                      "$ref": "#/components/schemas/IncidentDetail"
                    },
                    "permissions": {
                      "$ref": "#/components/schemas/SafetyHubIncidentPermissions"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request — investigator_id missing"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — not a manager, the incident is outside the caller's sites, an own-scoped token, or Safety Hub / the Incidents module disabled"
          },
          "404": {
            "description": "Incident not found (or not in the caller's business)"
          },
          "422": {
            "description": "Unprocessable — the incident is closed/cancelled (its investigator is frozen), or investigator_id is not an active admin/manager of this business"
          }
        }
      }
    },
    "/safety_hub/incidents/{id}/complete_investigation": {
      "post": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Complete an incident's investigation",
        "description": "Record an incident investigation's conclusions and close it out — the\nmockup's incident-detail manager panel \"Complete Investigation\" form, and\nthe native twin of the desktop\n`Apps::SafetyHub::IncidentsController#complete_investigation`.\n\n**Authority is MANAGER-ONLY within accessible sites**, unlike editing an\nincident (which the reporter may also do). The web offers this control to\nno persona but a **safety-hub manager** whose accessible sites include the\nincident — exactly the detail read's `permissions.can_manage_investigation`\nflag — so the API gates the same way: `safety_hub_manager?` AND the\nincident is within the caller's accessible sites. Because a completion is\nalways a management action, the wide **`write:safety_hub`** scope is\nrequired unconditionally — an own-scoped `write:own_safety_hub` token is\nrefused (there is no \"own\" narrow case here). Gated on the tenant's\n`incidents_enabled` module toggle.\n\n`findings` is **required** (an investigation record with no findings is not\nan investigation record, and the completion cannot be undone);\n`corrective_actions` is optional and round-trips to the investigation's\n`recommendations`. Both flow through `Incident#complete_investigation!` —\nthe same door the web uses — which, in one transaction, advances the\nincident to `investigation_completed`, records the findings on the\n`IncidentInvestigation` row (seeding one and resolving an investigator when\nnone is assigned), and writes the investigation-findings audit note onto\nthe activity timeline.\n\nThe completion is **one-way** — there is no reopen route and the edit door\ncannot walk `status` back — so an incident that is not `investigating`\n(still `reported`, or already `investigation_completed` / `closed` /\n`cancelled`) is refused with **422** rather than re-stamped over its own\nfindings. The response is the full incident detail (with the investigation\nnarrative, visible to the completing manager as a PII viewer) plus the\nviewer capability flags.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "findings"
                ],
                "properties": {
                  "findings": {
                    "type": "string",
                    "description": "The investigation's conclusions. Required and non-blank."
                  },
                  "corrective_actions": {
                    "type": "string",
                    "description": "Corrective actions taken, recorded as the investigation's `recommendations`. Optional."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Investigation completed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "incident": {
                      "$ref": "#/components/schemas/IncidentDetail"
                    },
                    "permissions": {
                      "$ref": "#/components/schemas/SafetyHubIncidentPermissions"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — not a site manager, the incident is outside the caller's sites, an own-scoped token, or Safety Hub / the Incidents module disabled"
          },
          "404": {
            "description": "Incident not found"
          },
          "422": {
            "description": "Unprocessable — findings are blank, the incident is not under investigation (already completed / not yet started), or no investigator could be resolved to record the findings against"
          }
        }
      }
    },
    "/safety_hub/observations": {
      "get": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List safety observations",
        "description": "Personal feed by DEFAULT: observations the caller submitted. Pass\n`team=true` for the business-wide feed, which requires Safety Hub\nmanager access.\n",
        "parameters": [
          {
            "name": "team",
            "in": "query",
            "description": "Set to `true` for the business-wide feed (manager-gated). Omit or\nset to `false` for the personal feed.\n",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "category",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Free-form category (e.g., PPE, Housekeeping)"
          },
          {
            "name": "observation_type",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "positive",
                "at_risk",
                "near_miss"
              ]
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "submitted",
                "under_review",
                "resolved",
                "closed"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "List of observations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "observations": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SafetyObservation"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/SafetyHubPaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          }
        }
      },
      "post": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Submit a safety observation",
        "description": "Submit a new safety observation — the native \"My Submission\" → camera →\n\"Continue without photo\" → incident form → tap **Observation** flow. The\nAPI twin of the web `Apps::SafetyHub::SafetyObservationsController#create`\nand mobile `#create_observation`.\n\n**Any member may file one**; the caller is recorded as the observer. Only\n`observation_type`, `category` and `description` are required — a phone\ncan post the bare observation form and **add photos later**. `observed_at`\ndefaults to now when omitted.\n\nPhotos ride in as multipart `photos[]` and are stored as MediaItem (the\nAI-vision / EXIF-strip pipeline fires and the detail timeline renders\nthem). A photo that fails AFTER the observation is saved is reported in\nthe `warnings` array rather than failing the submission. An **at-risk** or\n**near-miss** observation notifies the site's managers and safety officers\n(a positive observation is a commendation, not an alert).\n\n`anonymous` is honoured **only** when the tenant enables\n`allow_anonymous_observations`; otherwise the observer is always recorded.\nA `location_id` or `safety_observation_campaign_id` the caller's tenant\ndoes not own is dropped (not a 422). `status` cannot be set here — a new\nobservation always starts `submitted`.\n\nRequires the **write** scope: `write:safety_hub`, or `write:own_safety_hub`\nfor a frontline employee token. Gated on the tenant's\n`observations_enabled` module toggle.\n",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "safety_observation"
                ],
                "properties": {
                  "safety_observation": {
                    "$ref": "#/components/schemas/SafetyObservationCreate"
                  },
                  "anonymous": {
                    "type": "boolean",
                    "description": "Hide the observer identity. Honoured only when the tenant\nenables anonymous observations.\n"
                  },
                  "photos": {
                    "type": "array",
                    "description": "Photo/video uploads attached to the observation (optional).",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    }
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "safety_observation"
                ],
                "properties": {
                  "safety_observation": {
                    "$ref": "#/components/schemas/SafetyObservationCreate"
                  },
                  "anonymous": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Observation submitted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "observation": {
                      "$ref": "#/components/schemas/SafetyObservationDetail"
                    },
                    "warnings": {
                      "type": "array",
                      "description": "Per-photo attachment failures, if any (the observation was still saved).",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request — the safety_observation object is missing entirely"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — a read-only token, or Safety Hub / the Observations module disabled"
          },
          "422": {
            "description": "Unprocessable — a required field is blank or invalid",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "errors": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/safety_hub/observations/{id}": {
      "get": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get one safety observation",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Observation detail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "observation": {
                      "$ref": "#/components/schemas/SafetyObservationDetail"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Observation not found"
          }
        }
      },
      "patch": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Edit a safety observation",
        "description": "Edit ONE observation — the mockup's observation-detail \"⋯\" menu → \"Edit\nobservation\", and the native twin of the desktop\n`SafetyObservationsController#update`.\n\n**Authority** mirrors the desktop edit gate: the **observer** may correct\ntheir OWN observation, and a **site manager** may correct anyone's within\ntheir accessible sites. A member editing their own observation needs only\nthe narrow `write:own_safety_hub` scope; editing another user's via manager\nprivilege needs the wide `write:safety_hub` (an own-scoped token acting\nbeyond itself is refused). Gated on the tenant's `observations_enabled`\nmodule toggle.\n\n**`status`** is accepted only from a manager — a member's value is dropped\n(workflow state is not theirs to mass-assign). **`anonymous`** is a\nONE-WAY toggle: a reporter may anonymise an existing observation when the\ntenant enables `allow_anonymous_observations`, but never de-anonymise.\n**Photos** posted as multipart `photos[]` are ADDED through MediaItem\n(never replace the existing set); an upload that fails after the edit is\ncommitted is reported in `warnings` rather than failing the request.\n\n`PUT` is accepted as an alias of `PATCH`.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "safety_observation"
                ],
                "properties": {
                  "anonymous": {
                    "type": "boolean",
                    "description": "One-way — anonymise the observation (tenant policy permitting)."
                  },
                  "safety_observation": {
                    "type": "object",
                    "properties": {
                      "observation_type": {
                        "type": "string",
                        "enum": [
                          "positive",
                          "at_risk",
                          "near_miss"
                        ]
                      },
                      "category": {
                        "type": "string"
                      },
                      "description": {
                        "type": "string"
                      },
                      "action_taken": {
                        "type": "string"
                      },
                      "follow_up_required": {
                        "type": "boolean"
                      },
                      "follow_up_notes": {
                        "type": "string"
                      },
                      "specific_location": {
                        "type": "string"
                      },
                      "location_id": {
                        "type": "integer",
                        "description": "Site id in the caller's business (a foreign id is ignored)."
                      },
                      "safety_observation_campaign_id": {
                        "type": "integer"
                      },
                      "observed_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "status": {
                        "type": "string",
                        "enum": [
                          "submitted",
                          "under_review",
                          "resolved",
                          "closed"
                        ],
                        "description": "Manager-only; ignored for a member."
                      }
                    }
                  }
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "anonymous": {
                    "type": "boolean"
                  },
                  "photos[]": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "Photos/videos to ADD to the observation."
                  },
                  "safety_observation": {
                    "type": "object"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Observation updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "observation": {
                      "$ref": "#/components/schemas/SafetyObservationDetail"
                    },
                    "permissions": {
                      "type": "object",
                      "properties": {
                        "is_observer": {
                          "type": "boolean"
                        },
                        "is_manager": {
                          "type": "boolean"
                        },
                        "can_edit": {
                          "type": "boolean"
                        },
                        "can_complete_follow_up": {
                          "type": "boolean",
                          "description": "True only when a follow-up is outstanding AND the viewer is a site manager (mirrors the manager-only POST .../complete_follow_up gate)."
                        }
                      }
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Non-fatal issues (e.g. a photo that could not be attached)."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — not the observer/manager, an own-scoped token reaching beyond itself, or Safety Hub / the Observations module disabled"
          },
          "404": {
            "description": "Observation not found"
          },
          "422": {
            "description": "Unprocessable — validation failed (e.g. a blank description)"
          }
        }
      },
      "put": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Edit a safety observation (alias of PATCH)",
        "description": "Alias of `PATCH /safety_hub/observations/{id}`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Observation updated"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Observation not found"
          },
          "422": {
            "description": "Unprocessable"
          }
        }
      }
    },
    "/safety_hub/observations/{id}/complete_follow_up": {
      "post": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Complete an observation's follow-up",
        "description": "Mark ONE observation's follow-up as complete — the mockup's observation\ndetail \"Complete Follow-up\" button (\"Mark this follow-up as complete? The\nobservation will be marked resolved.\"), and the native twin of the desktop\n`SafetyObservationsController#mark_follow_up_complete`.\n\n**Authority is MANAGER-ONLY**, unlike editing an observation (which the\nobserver may also do). The web offers this control to no persona but a\n**safety-hub manager** within their accessible sites, so the API gates the\nsame way: `safety_hub_manager?` AND the observation is within the caller's\naccessible sites. Because a follow-up completion is always a management\naction, the wide **`write:safety_hub`** scope is required unconditionally —\nan own-scoped `write:own_safety_hub` token is refused (there is no \"own\"\nnarrow case here). Gated on the tenant's `observations_enabled` module\ntoggle.\n\nRoutes through `SafetyObservation#mark_follow_up_complete!` — the same door\nthe web uses — so it stamps `follow_up_completed_at` + `follow_up_completed_by`\nand advances `status` to `resolved` unless the observation is already\n`closed` (a closed observation records the completion but is NOT reopened).\nA follow-up that is not outstanding is refused with **422**: either the\nobservation has no follow-up flagged, or it was already completed (the\noriginal completer/timestamp audit is never overwritten). Takes no request\nbody.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Follow-up completed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "observation": {
                      "$ref": "#/components/schemas/SafetyObservationDetail"
                    },
                    "permissions": {
                      "type": "object",
                      "properties": {
                        "is_observer": {
                          "type": "boolean"
                        },
                        "is_manager": {
                          "type": "boolean"
                        },
                        "can_edit": {
                          "type": "boolean"
                        },
                        "can_complete_follow_up": {
                          "type": "boolean",
                          "description": "True only when a follow-up is outstanding AND the viewer is a site manager. After completion it is false (the follow-up is no longer outstanding)."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — not a manager, the observation is outside the caller's sites, an own-scoped token, or Safety Hub / the Observations module disabled"
          },
          "404": {
            "description": "Observation not found"
          },
          "422": {
            "description": "Unprocessable — no outstanding follow-up (none flagged, or already completed), or the observation is not in a completable state"
          }
        }
      }
    },
    "/safety_hub/toolbox_talks": {
      "get": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List toolbox talks (personal or team)",
        "description": "Default (`team` absent or false) — **personal feed**: only talks where\nthe current user is the facilitator OR appears in the talk's\n`expected_attendees` JSONB array. Sorted by `scheduled_at` ASC.\n\nNote: this is narrower than the desktop \"My Toolbox Talks\" view at\n`/apps/safety-hub/toolbox_talks/my`, which additionally surfaces past\ntalks the user attended without being pre-listed (via\n`toolbox_talk_attendances`) and sorts newest first. The API\ndeliberately scopes to facilitator + expected-attendee only.\n\n`team=true` — **team feed**: all toolbox talks in the business.\nRequires manager-level access (`manager_or_above?` OR safety-hub\napp-admin), matching the web \"Toolbox Talks\" admin surface. Sorted by\n`scheduled_at` DESC.\n\nCombinable with `status` and `scope` filters in either mode.\n",
        "parameters": [
          {
            "name": "team",
            "in": "query",
            "description": "Set to `true` to request the team feed (manager-gated). Omit or\nset to `false` for the personal feed.\n",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by lifecycle status",
            "schema": {
              "type": "string",
              "enum": [
                "scheduled",
                "in_progress",
                "completed",
                "cancelled"
              ]
            }
          },
          {
            "name": "scope",
            "in": "query",
            "description": "Filter by lifecycle scope",
            "schema": {
              "type": "string",
              "enum": [
                "upcoming",
                "completed"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "List of toolbox talks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "toolbox_talks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ToolboxTalk"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/SafetyHubPaginationMeta"
                    },
                    "scope": {
                      "type": "string",
                      "enum": [
                        "personal",
                        "team"
                      ],
                      "description": "Which scope the response was generated for."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — Safety Hub disabled, no access, or team=true requested by a non-manager"
          }
        }
      }
    },
    "/safety_hub/toolbox_talks/{id}": {
      "get": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get one toolbox talk",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Toolbox talk detail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "toolbox_talk": {
                      "$ref": "#/components/schemas/ToolboxTalkDetail"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Toolbox talk not found"
          }
        }
      }
    },
    "/safety_hub/topics": {
      "get": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List toolbox talk topics",
        "description": "Active toolbox-talk topic templates for the current business.",
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "List of topics",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "topics": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ToolboxTalkTopic"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/SafetyHubPaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          }
        }
      }
    },
    "/safety_hub/certifications": {
      "get": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List certification requirements",
        "description": "Returns certification requirements (templates) configured for the business.",
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "include_inactive",
            "in": "query",
            "description": "Set to \"true\" to include inactive requirements (otherwise only active are returned)",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "List of certification requirements",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "certifications": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SafetyCertificationRequirement"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/SafetyHubPaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          }
        }
      }
    },
    "/safety_hub/submissions": {
      "get": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Submissions feed (personal or team)",
        "description": "Returns a unified incidents + observations feed, sorted by\n`submitted_at` (record `created_at`) descending — most recent\nsubmissions first. Each row ships UI-ready metadata (icon, label,\ncolor tokens) so native-mobile clients don't have to re-derive the\nvisual treatment.\n\nDefault (`team` absent or false) — **personal feed**: only rows the\ncurrent user reported/submitted, with anonymous rows excluded. Mirrors\nthe desktop `/apps/safety-hub/submitted_by_me` action.\n\n`team=true` — **team feed**: all incidents and observations in the\nbusiness, including anonymous rows. Requires manager-level access\n(`manager_or_above?` OR safety-hub app-admin), matching the web\n\"Team > Incidents/Observations\" surface.\n\nThe response also includes a `summary` block with current-month tile\ncounts whose scope follows the `team` parameter: the personal feed\nreturns the current user's own submissions (anonymous excluded), and\nthe team feed returns every submission in the business (anonymous\nincluded) — so the tiles always match the rows the user is looking\nat. Each tile carries a `label`, `count`, `color` (Bootstrap token),\nand `icon` (stable icon-name token) so clients can render headline\nmetrics without computing them.\n\nHonors the per-module toggles (`incidents_enabled`,\n`observations_enabled`) from the Safety Hub marketplace-app\nconfiguration in both scopes; disabled modules contribute `0` to the\nsummary and emit no rows.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          },
          {
            "name": "team",
            "in": "query",
            "description": "Set to `true` to request the team feed (manager-gated). Omit or\nset to `false` for the personal feed.\n",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "kind",
            "in": "query",
            "description": "Optional filter that restricts the returned rows to a single\nsubmission kind. Omit to return both incidents and observations\n(default). Unknown values are ignored (both are returned). The\n`summary` tile counts always include BOTH incidents and\nobservations (subject to the per-module toggles) regardless of\nthis filter, so headline counts stay stable as clients toggle\nbetween kinds.\n",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "incident",
                "observation"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Submission feed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "submissions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SafetyHubSubmission"
                      }
                    },
                    "summary": {
                      "$ref": "#/components/schemas/SafetyHubSubmissionsSummary"
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/SafetyHubPaginationMeta"
                    },
                    "scope": {
                      "type": "string",
                      "enum": [
                        "personal",
                        "team"
                      ],
                      "description": "Which scope the response was generated for."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — Safety Hub disabled, no access, or team=true requested by a non-manager"
          }
        }
      }
    },
    "/safety_hub/corrective_actions": {
      "get": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Corrective actions register (personal or team)",
        "description": "The unified Corrective & Preventive Action register (Capa::Action) —\nthe native mirror of the desktop\n`Apps::SafetyHub::CorrectiveActionsController#index`.\n\nDefault (`team` absent or false) — **personal feed** (\"My Corrective\nActions\"): the corrective/preventive actions ASSIGNED TO the caller.\nMirrors the desktop `#visible_scope`'s non-manager branch. Leader Rounds\nissues are excluded (that register owns its own transitions).\n\n`team=true` — **team feed**: the whole register, requiring manager-level\naccess (`manager_or_above?` OR safety-hub app-admin) and site-scoped\nidentically to the desktop board (a site-restricted manager sees actions\nat their accessible sites, site-less actions, and anything assigned to\nthem). An own-scoped token (`read:own_safety_hub` without\n`read:safety_hub`) is bounded to the personal feed.\n\nGated on the tenant's `incidents_enabled` module toggle, exactly as the\ndesktop register and its navigation item are.\n\nRows are ordered open-work-first, then by due date ascending, then id\ndescending — the register's shared ordering authority\n(`Capa::Action.open_first_order`) so this feed and the web open on the\nsame first screen.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          },
          {
            "name": "team",
            "in": "query",
            "description": "Set to `true` for the team feed (manager-gated). Omit/false for the personal feed.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "A real status (`pending`, `in_progress`, `completed`, `cancelled`),\nor one of the pseudo-statuses `open` (pending+in_progress), `closed`\n(completed+cancelled), `overdue` (open actions past their due date),\nor `all` (no status filter). Omitted → no status filter. An\nunrecognized value returns 400. `overdue` is the native filter row's\nown chip (All · Pending · In Progress · Overdue · Completed ·\nCancelled), so the whole single-select row can be driven through\n`status`; it maps to the same scope as the `overdue=true` flag.\n",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "in_progress",
                "completed",
                "cancelled",
                "open",
                "closed",
                "overdue",
                "all"
              ]
            }
          },
          {
            "name": "overdue",
            "in": "query",
            "description": "Set to `true` to return only open actions past their due date.\nCombinable with a real `status` (e.g. `status=in_progress&overdue=true`);\non its own it is equivalent to `status=overdue`.\n",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "priority",
            "in": "query",
            "description": "One of low/medium/high/critical. Unrecognized values are ignored (no filter applied).",
            "schema": {
              "type": "string",
              "enum": [
                "low",
                "medium",
                "high",
                "critical"
              ]
            }
          },
          {
            "name": "action_type",
            "in": "query",
            "description": "One of the Capa action types. Unrecognized values are ignored.",
            "schema": {
              "type": "string",
              "enum": [
                "immediate",
                "short_term",
                "long_term",
                "preventive",
                "training",
                "policy_change",
                "equipment",
                "environmental"
              ]
            }
          },
          {
            "name": "assigned_to_id",
            "in": "query",
            "description": "Team feed only (`team=true`) — narrow the board to a single\nassignee. Ignored in the personal feed (already scoped to the caller).\n",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Corrective actions register slice",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "corrective_actions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SafetyHubCorrectiveAction"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/SafetyHubPaginationMeta"
                    },
                    "scope": {
                      "type": "string",
                      "enum": [
                        "personal",
                        "team"
                      ],
                      "description": "Which scope the response was generated for."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request — unrecognized status value"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — Safety Hub or the Incidents module disabled, no access, or team=true requested by a non-manager"
          }
        }
      }
    },
    "/safety_hub/corrective_actions/{id}": {
      "get": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get one corrective action",
        "description": "Full detail for ONE corrective/preventive action (Capa::Action) — the\nnative \"My Corrective Actions\" detail screen: the priority/status/type\nchips, the Action Details card (source, assignee, due date, description),\nthe Completion record, and the ISO 45001 effectiveness-verification\nrecord.\n\nVisible to exactly the personas the desktop\n`Apps::SafetyHub::CorrectiveActionsController#visible_scope` admits: the\naction's **assignee**, or a **safety-hub manager** whose accessible\nsites include it (incident-sourced rows inherit their incident's site;\nother rows are scoped by `location_id` with site-less rows kept; plus\nanything assigned directly to the caller). Leader Rounds-sourced actions\nare excluded (that ledger owns its own detail surface). An action the\ncaller may not reach returns 404 — it never confirms the row exists.\n\nBeyond the list card, the payload adds `verified_by`,\n`effectiveness_notes` and `days_until_due`, and a viewer-relative\n`permissions` block so a native client renders the right controls\n(Assign / Mark Complete / Verify) without a second round trip.\n\nGated on the tenant's `incidents_enabled` module toggle, exactly as the\nlist and the desktop register are.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Corrective action detail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "corrective_action": {
                      "$ref": "#/components/schemas/SafetyHubCorrectiveActionDetail"
                    },
                    "permissions": {
                      "$ref": "#/components/schemas/SafetyHubCorrectiveActionPermissions"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — Safety Hub or the Incidents module disabled, or no access"
          },
          "404": {
            "description": "Corrective action not found (or not visible to the caller)"
          }
        }
      }
    },
    "/safety_hub/corrective_actions/{id}/complete": {
      "post": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Mark a corrective action complete",
        "description": "Close out ONE corrective/preventive action (Capa::Action) from the\nnative \"My Corrective Actions\" detail — the mockup's \"Update Action\"\npanel, \"Mark Complete\" button and optional \"Completion notes…\" box.\n\nAuthority mirrors the desktop\n`Apps::SafetyHub::CorrectiveActionsController#complete` and the detail\nendpoint's `permissions.can_complete`: a **safety-hub manager** OR the\naction's **assignee**, on an **open** (pending / in_progress) action.\nThe action is resolved through the same desktop `#visible_scope` the\ndetail read uses, so an action the caller may not reach is a 404.\n\nRoutes through `Capa::Action#mark_completed!` — the same door the web\nuses — so an incident-sourced action also logs an `IncidentUpdate` audit\nrow on its parent incident, and a cancelled / already-completed action is\nrefused (422) rather than resurrected.\n\nRequires the **write** scope: `write:safety_hub`, or `write:own_safety_hub`\nwhen completing your OWN assigned action. Completing another user's action\nvia manager privilege requires the wide `write:safety_hub`. Gated on the\ntenant's `incidents_enabled` module toggle.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "resolution_notes": {
                    "type": "string",
                    "maxLength": 4000,
                    "description": "Optional completion notes (\"Completion notes…\" in the\nmockup). A blank/omitted value preserves any resolution note\nalready on file; it is never erased.\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Corrective action completed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "corrective_action": {
                      "$ref": "#/components/schemas/SafetyHubCorrectiveActionDetail"
                    },
                    "permissions": {
                      "$ref": "#/components/schemas/SafetyHubCorrectiveActionPermissions"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — not the assignee or a manager, an own-scoped token acting on another user's action, or Safety Hub / the Incidents module disabled"
          },
          "404": {
            "description": "Corrective action not found (or not visible to the caller)"
          },
          "422": {
            "description": "Unprocessable — the action is already completed/cancelled, or the notes exceed the 4000-character limit"
          }
        }
      }
    },
    "/safety_hub/corrective_actions/{id}/assignee": {
      "patch": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Reassign a corrective action",
        "description": "Reassign ONE open corrective/preventive action (Capa::Action) to a\ndifferent business user — the mockup's \"Assign to\" picker on the \"My\nCorrective Actions\" detail.\n\nAuthority mirrors the detail endpoint's `permissions.can_assign`:\n**manager-only**, on an **open** (pending / in_progress) action (a member,\neven the current assignee, cannot reassign — it is a management decision).\nA completed / cancelled action's assignee is its \"closed out by\"\nattribution, so its ownership is frozen and reassignment returns 422.\n\nRoutes through `Capa::Action#assign!` (moves a pending action to\nin_progress and notifies the new assignee); the model's cross-tenant FK\nguard and this endpoint both reject an assignee who is not a member of the\nbusiness.\n\nRequires the **write** scope: `write:safety_hub` (reassigning is a manager\naction beyond the caller's own data, so the wide scope is required; a\nnarrow `write:own_safety_hub` token is refused). Gated on the tenant's\n`incidents_enabled` module toggle.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "assigned_to_id"
                ],
                "properties": {
                  "assigned_to_id": {
                    "type": "integer",
                    "description": "Id of the business user to assign the action to."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Corrective action reassigned",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "corrective_action": {
                      "$ref": "#/components/schemas/SafetyHubCorrectiveActionDetail"
                    },
                    "permissions": {
                      "$ref": "#/components/schemas/SafetyHubCorrectiveActionPermissions"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request — assigned_to_id missing"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — not a manager, an own-scoped token, or Safety Hub / the Incidents module disabled"
          },
          "404": {
            "description": "Corrective action not found (or not visible to the caller)"
          },
          "422": {
            "description": "Unprocessable — the action is closed (its assignee is frozen), or assigned_to_id is not a member of this business"
          }
        }
      }
    },
    "/safety_hub/permits": {
      "get": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List permits to work (\"My Permits\")",
        "description": "The caller's Permits to Work.\n\nPersonal feed by DEFAULT (\"My Permits\"): permits the caller REQUESTED\nor AUTHORISED. Pass `team=true` for the whole board, which requires\nSafety Hub manager access and is site-scoped to the manager's\naccessible sites (plus site-less permits and anything they are\npersonally on).\n\nRequires the Permits to Work module to be enabled for the business.\n",
        "parameters": [
          {
            "name": "team",
            "in": "query",
            "description": "Set to `true` for the whole board (manager-gated + site-scoped).\nOmit or set to `false` for the personal \"My Permits\" feed.\n",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by status. Accepts a real permit status\n(`draft`, `requested`, `approved`, `suspended`, `closed`,\n`cancelled`, `expired`) or one of the derived board views:\n`active` (issued and inside its work window), `awaiting`\n(requested / awaiting approval), `overrun` (issued and past its end\ntime), or `all` (no status filter). Omitted → the OPEN board\n(draft / requested / approved / suspended), matching the web\nsurfaces' default. An unrecognized or malformed value falls back to\nthe OPEN board.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "permit_type",
            "in": "query",
            "description": "Filter by permit type — one of `hot_work`, `confined_space`,\n`working_at_height`, `electrical`, `excavation`, `lifting`,\n`general`.\n",
            "schema": {
              "type": "string",
              "enum": [
                "hot_work",
                "confined_space",
                "working_at_height",
                "electrical",
                "excavation",
                "lifting",
                "general"
              ]
            }
          },
          {
            "name": "location_id",
            "in": "query",
            "description": "Narrow the board to a single site.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Page size, clamped to [1, 100]. Defaults to 25.",
            "schema": {
              "type": "integer",
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Permit board slice",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "permits": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SafetyHubPermit"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/SafetyHubPaginationMeta"
                    },
                    "scope": {
                      "type": "string",
                      "enum": [
                        "personal",
                        "team"
                      ],
                      "description": "Which scope the response was generated for."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — Safety Hub or the Permits to Work module disabled, no access, or team=true requested by a non-manager"
          }
        }
      },
      "post": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Raise a draft permit to work",
        "description": "Raise a DRAFT permit to work — the native mirror of the \"My Permits\"\nboard's \"Request Permit\" (+) button and the API twin of the desktop\n`Apps::SafetyHub::PermitsController#create`. (Mobile-web has no\npermit-create route — permits are requested and authorised on the\ndesktop board.)\n\n**Any member may raise one** — creation is gated on nothing but the\nPermits module toggle, and the web offers the button to every persona\n(only the page title differs: \"My Permits\" vs \"Permits to Work\"). The\ncaller is always recorded as the **requester** and the permit is born a\n**draft** — issuing it (`request` → a DIFFERENT manager `approve`s) is a\nseparate two-person control, never part of creation, so `status` is not\naccepted here.\n\n`permit_type`, `title`, `starts_at` and `ends_at` are required. A bad\nwork window (end before start, or longer than 14 days), a cross-tenant\n`location_id`/`vendor_id`, or a missing field is refused with a 422 and\nthe payload is left intact — never a silent save. `permit_number` is\nassigned automatically (`PTW-<year>-NNNN`, per business-year).\n\n**`precautions`** are the authoriser's confirmations — a non-manager\ncaller's `precautions` key is dropped, so a requester can never pre-tick\non their own draft the controls that gate issue.\n\nRequires the **write** scope: `write:safety_hub`, or `write:own_safety_hub`\n(the by-hand employee grant). Gated on the tenant's `permits_enabled`\nmodule toggle.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "work_permit"
                ],
                "properties": {
                  "work_permit": {
                    "type": "object",
                    "required": [
                      "permit_type",
                      "title",
                      "starts_at",
                      "ends_at"
                    ],
                    "properties": {
                      "permit_type": {
                        "type": "string",
                        "enum": [
                          "hot_work",
                          "confined_space",
                          "working_at_height",
                          "electrical",
                          "excavation",
                          "lifting",
                          "general"
                        ]
                      },
                      "title": {
                        "type": "string",
                        "maxLength": 255
                      },
                      "description": {
                        "type": "string"
                      },
                      "area": {
                        "type": "string"
                      },
                      "location_id": {
                        "type": "integer",
                        "description": "Site id in the caller's business (a foreign id is refused with a 422)."
                      },
                      "vendor_id": {
                        "type": "integer",
                        "description": "Contractor id in the caller's business (a foreign id is refused with a 422)."
                      },
                      "starts_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "ends_at": {
                        "type": "string",
                        "format": "date-time",
                        "description": "Must be after starts_at and within 14 days of it."
                      },
                      "isolations": {
                        "type": "string"
                      },
                      "ppe": {
                        "type": "string"
                      },
                      "gas_test": {
                        "type": "string"
                      },
                      "emergency_arrangements": {
                        "type": "string"
                      },
                      "hazards": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        },
                        "description": "The requester's identified hazards."
                      },
                      "precautions": {
                        "type": "object",
                        "additionalProperties": {
                          "type": "boolean"
                        },
                        "description": "Authoriser-only — a map of precaution text → confirmed. Dropped for a non-manager caller."
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Draft permit created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "permit": {
                      "$ref": "#/components/schemas/SafetyHubPermitDetail"
                    },
                    "permissions": {
                      "type": "object",
                      "properties": {
                        "is_requester": {
                          "type": "boolean"
                        },
                        "is_manager": {
                          "type": "boolean"
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "True when the caller may save — the requester of a fresh draft, or a manager while the permit is open."
                        },
                        "can_confirm_precautions": {
                          "type": "boolean",
                          "description": "True only for a manager while the permit is open — the authoriser-only precaution checklist is editable."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request — the work_permit payload is missing entirely"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — the token lacks the write scope, or Safety Hub / the Permits to Work module is disabled"
          },
          "422": {
            "description": "Unprocessable — validation failed (a missing field, an inverted/over-long window, or a cross-tenant site/contractor)"
          }
        }
      }
    },
    "/safety_hub/permits/{id}": {
      "get": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get one permit to work",
        "description": "Full permit detail — the validity window, requester (issuer),\nauthoriser (approver), the type's hazards and the precaution checklist\nwith each control's confirmed state, plus the closure record. Viewable\nby the requester, the authoriser, or a manager whose accessible sites\ninclude the permit's site (site-less permits stay manager-visible).\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Permit detail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "permit": {
                      "$ref": "#/components/schemas/SafetyHubPermitDetail"
                    },
                    "permissions": {
                      "type": "object",
                      "properties": {
                        "is_requester": {
                          "type": "boolean"
                        },
                        "is_manager": {
                          "type": "boolean"
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "True when the caller may save — the requester before issue, or a manager while the permit is open."
                        },
                        "can_confirm_precautions": {
                          "type": "boolean",
                          "description": "True only for a manager while the permit is open — the authoriser-only precaution checklist is editable."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — you may only view permits you requested or authorised"
          },
          "404": {
            "description": "Permit not found"
          }
        }
      },
      "patch": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Save a permit to work",
        "description": "Save ONE permit's working record — the mockup's permit-detail \"Save\"\nbutton, and the native twin of the desktop `PermitsController#update`.\n\nONE door, two personas, exactly as the shared web save: the **requester**\nfills the hazard checklist and the control notes (isolations, PPE, gas\ntest, emergency arrangements) until the permit is issued, and a **manager**\n(the authoriser) confirms the **precaution** checklist — the gate on\nissue — while the permit is open.\n\n**Authority** mirrors the desktop edit gate: the **requester** may save\ntheir OWN permit while it is still `draft`/`requested` (they lose the pen\nonce it is issued — the type, window, site and contractor also freeze\nthen), and a **site manager** may save any permit within their accessible\nsites while it is open. A member saving their own permit needs only the\nnarrow `write:own_safety_hub` scope; saving another user's via manager\nprivilege needs the wide `write:safety_hub` (an own-scoped token acting\nbeyond itself is refused). Gated on the tenant's `permits_enabled` module\ntoggle.\n\n**`precautions`** are the authoriser's — a non-manager's `precautions`\nkey is dropped before the write, so a requester can never sign off their\nown controls. **Issuing** the permit (`draft`/`requested` → `approved`) is\na SEPARATE control — this action only saves the working record and never\nadvances the lifecycle. A cross-tenant `location_id`/`vendor_id` is\nrefused with a 422 (the model's belongs-to-business validation).\n\n`PUT` is accepted as an alias of `PATCH`.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "work_permit"
                ],
                "properties": {
                  "work_permit": {
                    "type": "object",
                    "properties": {
                      "title": {
                        "type": "string"
                      },
                      "description": {
                        "type": "string"
                      },
                      "area": {
                        "type": "string"
                      },
                      "permit_type": {
                        "type": "string",
                        "enum": [
                          "hot_work",
                          "confined_space",
                          "working_at_height",
                          "electrical",
                          "excavation",
                          "lifting",
                          "general"
                        ],
                        "description": "Frozen once the permit is issued."
                      },
                      "location_id": {
                        "type": "integer",
                        "description": "Site id in the caller's business (a foreign id is refused with a 422). Frozen once issued."
                      },
                      "vendor_id": {
                        "type": "integer",
                        "description": "Contractor id in the caller's business. Frozen once issued."
                      },
                      "starts_at": {
                        "type": "string",
                        "format": "date-time",
                        "description": "Frozen once issued."
                      },
                      "ends_at": {
                        "type": "string",
                        "format": "date-time",
                        "description": "Frozen once issued."
                      },
                      "isolations": {
                        "type": "string"
                      },
                      "ppe": {
                        "type": "string"
                      },
                      "gas_test": {
                        "type": "string"
                      },
                      "emergency_arrangements": {
                        "type": "string"
                      },
                      "hazards": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        },
                        "description": "The requester's identified hazards (replaces the set)."
                      },
                      "precautions": {
                        "type": "object",
                        "additionalProperties": {
                          "type": "boolean"
                        },
                        "description": "Authoriser-only — a map of precaution text → confirmed. Dropped for a non-manager caller. Only controls in the permit type's catalog are recorded."
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Permit saved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "permit": {
                      "$ref": "#/components/schemas/SafetyHubPermitDetail"
                    },
                    "permissions": {
                      "type": "object",
                      "properties": {
                        "is_requester": {
                          "type": "boolean"
                        },
                        "is_manager": {
                          "type": "boolean"
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "True when the caller may save — the requester before issue, or a manager while the permit is open."
                        },
                        "can_confirm_precautions": {
                          "type": "boolean",
                          "description": "True only for a manager while the permit is open — the authoriser-only precaution checklist is editable."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — not the requester (before issue) or a manager, an own-scoped token reaching beyond itself, or Safety Hub / the Permits module disabled"
          },
          "404": {
            "description": "Permit not found"
          },
          "422": {
            "description": "Unprocessable — validation failed (e.g. a blank title, or a cross-tenant / frozen field)"
          }
        }
      },
      "put": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Save a permit to work (alias of PATCH)",
        "description": "Alias of `PATCH /safety_hub/permits/{id}`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Permit saved"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Permit not found"
          },
          "422": {
            "description": "Unprocessable"
          }
        }
      }
    },
    "/safety_hub/permits/{id}/request_approval": {
      "post": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Send a permit for approval",
        "description": "Send ONE draft permit for approval — the mockup's permit-detail\n\"Request approval\" button (shown for a `draft` permit to a caller who may\nedit it), and the native twin of the desktop\n`Apps::SafetyHub::PermitsController#request_approval`. Moves the permit\n`draft` → `requested` and notifies the authorisers, exactly as the web does.\n\n**Authority is the SAME door the save uses** — the desktop wires both\n\"Request approval\" and the working-record save behind one edit gate — so\nthis action gates identically: the **requester** while the permit is still\n`draft`/`requested` (own data → the narrow `write:own_safety_hub` scope\nsuffices), OR a **site manager** whose accessible sites include the permit\nwhile it is open (acting on another user's permit via manager privilege\nneeds the wide `write:safety_hub` — an own-scoped token reaching beyond\nitself is refused). Gated on the tenant's `permits_enabled` module toggle.\n\nThe lifecycle guard is the model's (`WorkPermit#request!`): only a **draft**\ncan be requested. A `requested`/`approved`/`suspended`/`closed`/`cancelled`/\n`expired` permit is refused with **422** — the button's own draft-only\nvisibility is enforced server-side and never trusted from the client. Takes\nno request body.\n\nThe response re-reads the full permit detail (same eager-load set as the\ndetail read — no N+1) with the refreshed viewer permissions, so the client\nrepaints the detail screen — the new `requested` state and the now-absent\nRequest-approval affordance — without a second round trip.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Approval requested (permit is now `requested`)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "permit": {
                      "$ref": "#/components/schemas/SafetyHubPermitDetail"
                    },
                    "permissions": {
                      "type": "object",
                      "properties": {
                        "is_requester": {
                          "type": "boolean"
                        },
                        "is_manager": {
                          "type": "boolean"
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "True when the caller may save — the requester before issue, or a manager while the permit is open."
                        },
                        "can_confirm_precautions": {
                          "type": "boolean",
                          "description": "True only for a manager while the permit is open — the authoriser-only precaution checklist is editable."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — not the requester (before issue) or a manager, an own-scoped token reaching beyond itself, or Safety Hub / the Permits module disabled"
          },
          "404": {
            "description": "Permit not found"
          },
          "422": {
            "description": "Unprocessable — the permit is not a draft (only a draft can be sent for approval)"
          }
        }
      }
    },
    "/safety_hub/permits/{id}/cancel": {
      "post": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Cancel a permit",
        "description": "Cancel ONE permit to work — the mockup's permit-detail red \"Cancel\"\ncontrol (shown for a non-terminal permit to a caller who may edit it), and\nthe native twin of the desktop `Apps::SafetyHub::PermitsController#cancel`.\nCancellation is **terminal**: it flips any still-open permit\n(`draft`/`requested`/`approved`/`suspended`) straight to `cancelled`,\nrecords the reason, and notifies the permit holders (requester +\nauthoriser, except the actor), exactly as the web does. It is the \"scrap\nthis permit\" door — because the model freezes a permit's core fields once\nit is issued, the sanctioned way to undo an issued permit is to cancel it\nand raise a fresh one.\n\n**Authority is the SAME door the save/request use** — the desktop wires\ncancel, the working-record save, and \"Request approval\" behind one edit\ngate — so this action gates identically: the **requester** while the permit\nis still `draft`/`requested` (own data → the narrow `write:own_safety_hub`\nscope suffices), OR a **site manager** whose accessible sites include the\npermit while it is open (acting on another user's permit via manager\nprivilege needs the wide `write:safety_hub` — an own-scoped token reaching\nbeyond itself is refused). A `closed`/`cancelled`/`expired` permit is\ncancellable by nobody — both arms of the gate reject it (**403**). Gated on\nthe tenant's `permits_enabled` module toggle.\n\nThe response re-reads the full permit detail (same eager-load set as the\ndetail read — no N+1) with the refreshed viewer permissions, so the client\nrepaints the detail screen — the new `cancelled` state and `cancelled_reason`\n— without a second round trip.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "description": "Optional free-text reason recorded on `cancelled_reason`. Defaults to \"Cancelled by {name}\" when blank."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Permit cancelled (permit is now `cancelled`)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "permit": {
                      "$ref": "#/components/schemas/SafetyHubPermitDetail"
                    },
                    "permissions": {
                      "type": "object",
                      "properties": {
                        "is_requester": {
                          "type": "boolean"
                        },
                        "is_manager": {
                          "type": "boolean"
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "False once cancelled — a terminal permit is editable by nobody."
                        },
                        "can_confirm_precautions": {
                          "type": "boolean",
                          "description": "False once cancelled — the permit is no longer open."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — not the requester (before issue) or a manager, the permit is already closed/cancelled/expired, an own-scoped token reaching beyond itself, or Safety Hub / the Permits module disabled"
          },
          "404": {
            "description": "Permit not found"
          },
          "422": {
            "description": "Unprocessable — the permit could not be cancelled (e.g. a concurrent close/expire made it terminal first)"
          }
        }
      }
    },
    "/safety_hub/knowledge_base": {
      "get": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Safety Knowledge Base (list, filter & search)",
        "description": "The Safety Knowledge Base — the native mirror of the desktop\n`Apps::SafetyHub::KnowledgeBaseController` and the mockup's \"Knowledge\nBase\" tab. Employee self-service content, so it is available to EVERY\npersona (no `team=` param).\n\nThere is no personal/team split. Instead, exactly as the desktop\n`#base_kb_scope` does, a **manager** (manager-or-above OR the safety-hub\napp-admin) sees every status — including `draft`, `failed` and\n`archived` editorial content — while a **member** sees only `active`\n(published) articles. Global system safety content is included for\nboth.\n\nFree-text search (`q`) is a token-AND ILIKE across title / question /\nanswer / content (word order does not matter). Results are ordered by\nthe model's display order, then most-recent, with an id tiebreak so a\ntie group cannot drop or duplicate a row across a page boundary.\n\n`categories` echoes the seven category chips, each with an un-paginated\ncount (respecting the `source_type` filter but NOT the `category`\nfilter, so a chip's badge and the page it opens describe the same set),\nplus an `all` total.\n\nRequires Safety Hub to be enabled for the business.\n",
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "description": "Filter to one Safety Hub category. An unknown value returns 400\n(it is never silently matched). A malformed container shape is\nignored (treated as no filter).\n",
            "schema": {
              "type": "string",
              "enum": [
                "emergency_procedures",
                "hazard_recognition",
                "ppe_guides",
                "safe_work_procedures",
                "incident_response",
                "toolbox_talk_content",
                "general_safety"
              ]
            }
          },
          {
            "name": "source_type",
            "in": "query",
            "description": "Filter by content type. An unknown value is ignored (treated as no\nfilter).\n",
            "schema": {
              "type": "string",
              "enum": [
                "faq",
                "document",
                "url",
                "video"
              ]
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Free-text search across title / question / answer / content (token-AND ILIKE).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Page size, clamped to [1, 100]. Defaults to 25.",
            "schema": {
              "type": "integer",
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of knowledge base articles",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "knowledge_base": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SafetyHubKnowledgeBaseEntry"
                      }
                    },
                    "categories": {
                      "type": "array",
                      "description": "The category chips (an \"all\" total plus each of the seven categories) with un-paginated counts.",
                      "items": {
                        "$ref": "#/components/schemas/SafetyHubKbCategoryFacet"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/SafetyHubPaginationMeta"
                    },
                    "filters": {
                      "type": "object",
                      "description": "Echo of the filters actually applied.",
                      "properties": {
                        "category": {
                          "type": "string",
                          "nullable": true
                        },
                        "source_type": {
                          "type": "string",
                          "nullable": true
                        },
                        "q": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request — unknown category"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — Safety Hub disabled or no access"
          }
        }
      }
    },
    "/safety_hub/knowledge_base/{id}": {
      "get": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get one knowledge base article",
        "description": "Full article detail — title, category, status, the primary\nquestion/answer, any additional Q&A pairs (`faq_items`), the extracted\nbody (`content`) for document/url/video entries, a safe external\n`source_url`, attached-file metadata, and the author + timestamps.\n\nA member requesting a non-active article (draft / failed / archived)\nreceives 404 — the same content boundary the list enforces; a manager\nmay open any status.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Knowledge base article detail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "knowledge_base_entry": {
                      "$ref": "#/components/schemas/SafetyHubKnowledgeBaseEntryDetail"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — Safety Hub disabled or no access"
          },
          "404": {
            "description": "Article not found"
          }
        }
      }
    },
    "/onboarding/my_plan": {
      "get": {
        "tags": [
          "Onboarding"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get the caller's onboarding plan (phases + items)",
        "description": "Returns the caller's active `RecruitingOnboardingPlan` (status in\n`active`, `preboarding`, or `draft`) bucketed into the six fixed\nphases. Each phase carries its own `items` array, per-phase\n`progress` percent, and `items_total`/`items_completed` counts.\n\nItem types covered: `task`, `form`, `document`, `checkpoint`,\n`survey`, `training`. The `id` field is a stable composite\n(`<type>_<record_id>`) — clients should treat it opaquely.\n\nPhase boundaries (relative to `plan.start_date`):\n  * `preboarding` — day_offset < 0\n  * `day_1`       — day_offset 0..1\n  * `week_1`      — day_offset 2..6\n  * `month_1`     — day_offset 7..29\n  * `month_2_3`   — day_offset 30..89\n  * `beyond`      — day_offset >= 90\n\nItems are sorted within each phase by `(day_offset ASC, name ASC)`.\n\n`days_overdue` is populated only when the item's `due_date` is in\nthe past AND its status is NOT one of `completed`, `verified`, or\n`skipped` — matching the red \"X days overdue\" badge in the desktop\nview. Otherwise `null`.\n\n`overall_progress` mirrors `RecruitingOnboardingPlan#completion_percentage`\n(the same value the desktop dashboard shows). It applies partial\ncredit (0.75) for documents in the `uploaded` state, so it can\ndiverge slightly from a naïve completed/total ratio over all phases.\n",
        "responses": {
          "200": {
            "description": "The caller's onboarding plan with phases and items.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "plan",
                    "phases"
                  ],
                  "properties": {
                    "plan": {
                      "type": "object",
                      "required": [
                        "id",
                        "status",
                        "overall_progress"
                      ],
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 42
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "active",
                            "preboarding",
                            "draft"
                          ],
                          "example": "preboarding"
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "example": "2026-04-25"
                        },
                        "target_completion_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "example": "2026-07-24"
                        },
                        "days_remaining": {
                          "type": "integer",
                          "nullable": true,
                          "example": 39
                        },
                        "overall_progress": {
                          "type": "integer",
                          "description": "Plan-wide completion percent (0-100). Mirrors\nRecruitingOnboardingPlan#completion_percentage\nincluding partial credit for uploaded-but-unverified\ndocuments.\n",
                          "example": 29
                        }
                      }
                    },
                    "phases": {
                      "type": "array",
                      "description": "Always six entries, in fixed order. Empty phases\nare returned with `items: []` and `progress: 0` so\nthe client can render every row unconditionally.\n",
                      "items": {
                        "type": "object",
                        "required": [
                          "key",
                          "label",
                          "progress",
                          "items_total",
                          "items_completed",
                          "items"
                        ],
                        "properties": {
                          "key": {
                            "type": "string",
                            "enum": [
                              "preboarding",
                              "day_1",
                              "week_1",
                              "month_1",
                              "month_2_3",
                              "beyond"
                            ],
                            "example": "preboarding"
                          },
                          "label": {
                            "type": "string",
                            "example": "Pre-boarding"
                          },
                          "description": {
                            "type": "string",
                            "example": "Before the start date"
                          },
                          "progress": {
                            "type": "integer",
                            "description": "Per-phase completion percent (0-100).",
                            "example": 60
                          },
                          "items_total": {
                            "type": "integer",
                            "example": 5
                          },
                          "items_completed": {
                            "type": "integer",
                            "example": 3
                          },
                          "items": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "required": [
                                "id",
                                "type",
                                "title",
                                "status"
                              ],
                              "properties": {
                                "id": {
                                  "type": "string",
                                  "description": "Stable composite (`<type>_<record_id>`).",
                                  "example": "document_19"
                                },
                                "type": {
                                  "type": "string",
                                  "enum": [
                                    "task",
                                    "form",
                                    "document",
                                    "checkpoint",
                                    "survey",
                                    "training"
                                  ],
                                  "example": "document"
                                },
                                "title": {
                                  "type": "string",
                                  "example": "Upload BOSIET / OPITO Survival Certificate"
                                },
                                "due_date": {
                                  "type": "string",
                                  "format": "date",
                                  "nullable": true,
                                  "example": "2026-05-01"
                                },
                                "status": {
                                  "type": "string",
                                  "description": "Raw status from the underlying record (with the\ntask model's `display_status` collapse applied\nfor tasks). The full set:\n`pending`, `in_progress`, `submitted`, `scheduled`,\n`completed`, `verified`, `uploaded`, `rejected`,\n`overdue`, `skipped`.\n",
                                  "example": "pending"
                                },
                                "status_label": {
                                  "type": "string",
                                  "description": "Humanized form of `status` (e.g. \"In progress\").",
                                  "example": "Pending"
                                },
                                "status_color": {
                                  "type": "string",
                                  "description": "Bootstrap contextual color token for\n`status`, identical to the desktop timeline\nbadge. Prefix with `bg-`/`text-` to render.\n",
                                  "enum": [
                                    "success",
                                    "info",
                                    "warning",
                                    "danger",
                                    "secondary"
                                  ],
                                  "example": "warning"
                                },
                                "icon": {
                                  "type": "string",
                                  "description": "FontAwesome icon class for the item type.",
                                  "example": "fas fa-tasks"
                                },
                                "icon_color": {
                                  "type": "string",
                                  "description": "Bootstrap contextual color token for the\nitem-type `icon`.\n",
                                  "example": "success"
                                },
                                "required": {
                                  "type": "boolean",
                                  "example": true
                                },
                                "assignee": {
                                  "type": "string",
                                  "nullable": true,
                                  "description": "Display name of the assignee/owner (tasks + checkpoints only).",
                                  "example": null
                                },
                                "days_overdue": {
                                  "type": "integer",
                                  "nullable": true,
                                  "description": "Days past due. `null` for items that are not\noverdue OR are in a terminal state\n(`completed` / `verified` / `skipped`).\n",
                                  "example": 43
                                },
                                "web_url": {
                                  "type": "string",
                                  "format": "uri",
                                  "description": "Absolute URL of the item's detail page in the\ndesktop Onboarding Hub — the same link the\ndesktop timeline renders. Surveys and\ntrainings have no standalone detail page, so\nthey point at the plan page.\n",
                                  "example": "https://officechat-dev.workforce.mangoapps.com/apps/onboarding_hub/documents/19"
                                },
                                "mobile_url": {
                                  "type": "string",
                                  "format": "uri",
                                  "description": "Absolute URL of the item's detail page on the\nmobile-web surface (`/m/apps/onboarding-hub/...`).\nOnly task & document ship a dedicated mobile\ndetail page; every other type falls back to\nthe mobile plan page so the link always\nresolves.\n",
                                  "example": "https://officechat-dev.workforce.mangoapps.com/m/apps/onboarding-hub/documents/19"
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required."
          },
          "404": {
            "description": "The caller has no active onboarding plan.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "no_active_plan"
                        },
                        "message": {
                          "type": "string",
                          "example": "You do not have an active onboarding plan."
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/onboarding/team": {
      "get": {
        "tags": [
          "Onboarding"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Team Onboarding summary (overlapping bucket counts)",
        "description": "Aggregates onboarding items across the caller's active TEAM plans\n— the plans where the caller is the `hiring_manager` and status\nis one of `active`, `preboarding`, `draft`. Returns seven\n**overlapping** counts the mobile widget renders:\n\n  - Six PHASE buckets (`preboarding`, `day_1`, `week_1`,\n    `month_1`, `month_2_3`, `beyond`) — each item is counted in\n    exactly one phase bucket. The six phase counts sum to\n    `total_items`.\n  - One OVERDUE bucket — cross-phase count of every item past\n    its due date (`TimelinePhasesService.item_overdue?`).\n    **Overdue items are ALSO counted in their phase bucket**,\n    so the seven buckets do NOT sum to `total_items` — overdue\n    is an overlapping subset, not a separate slice.\n\nCross-endpoint invariants:\n\n  - `/team` `buckets[phase_X].count` ≡ `/team/items?phase=X`\n    `meta.total_count`\n  - `/team` `buckets[overdue].count` ≡ `/team/items?status=overdue`\n    `meta.total_count`\n\nAlways returns HTTP 200, even when the caller has zero team plans\n— all `buckets[].count` are 0 and `team.plan_count` is 0. The\nwidget hides itself client-side when `team.plan_count == 0`.\n",
        "responses": {
          "200": {
            "description": "Aggregated team onboarding counts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "team",
                    "buckets",
                    "total_items"
                  ],
                  "properties": {
                    "team": {
                      "type": "object",
                      "required": [
                        "plan_count",
                        "member_count"
                      ],
                      "properties": {
                        "plan_count": {
                          "type": "integer",
                          "description": "Number of active plans the caller manages.",
                          "example": 4
                        },
                        "member_count": {
                          "type": "integer",
                          "description": "Distinct new-hires across those plans. Differs\nfrom `plan_count` only in the rare case where a\nsingle person has more than one active plan.\n",
                          "example": 4
                        }
                      }
                    },
                    "buckets": {
                      "type": "array",
                      "description": "Seven entries in fixed order: `overdue` first, then\nthe six phases chronologically. The overdue bucket\nOVERLAPS with the phase buckets (an overdue day_1\nitem counts in BOTH `overdue` AND `day_1`). Empty\nbuckets are still present with `count: 0` so the\nclient can render every legend row unconditionally.\n",
                      "items": {
                        "type": "object",
                        "required": [
                          "key",
                          "label",
                          "count"
                        ],
                        "properties": {
                          "key": {
                            "type": "string",
                            "enum": [
                              "overdue",
                              "preboarding",
                              "day_1",
                              "week_1",
                              "month_1",
                              "month_2_3",
                              "beyond"
                            ],
                            "example": "overdue"
                          },
                          "label": {
                            "type": "string",
                            "example": "Overdue"
                          },
                          "count": {
                            "type": "integer",
                            "example": 14
                          }
                        }
                      }
                    },
                    "total_items": {
                      "type": "integer",
                      "description": "Distinct count of items across the team. Equal to\nthe sum of the six PHASE bucket counts (each item is\nin exactly one phase). The `overdue` bucket is a\nsubset and does NOT contribute to this sum.\n",
                      "example": 62
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required."
          }
        }
      }
    },
    "/onboarding/team/items": {
      "get": {
        "tags": [
          "Onboarding"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Team Onboarding drill-in (paginated items, phase + status filters)",
        "description": "Returns the line items in a manager's team plans, filtered by\ntwo independent axes — `phase` and `status` — plus optional\nfree-text search. Powers the mobile widget's drill-in modal\nwhen a manager taps a pie slice or changes the dropdowns inside\nthe modal.\n\nFilters compose. `phase=day_1&status=overdue` returns day_1\nitems that are also past due. `phase=all&status=overdue` (the\ncommon \"tap Overdue slice\" path) returns every overdue item\nacross all phases, matching the screenshot's \"Phase: Any,\nStatus: Overdue\" view.\n\nStatus partition (canonical 3-way collapse via\n`TimelinePhasesService.item_status_group`):\n\n  * `overdue`   — item past `due_date` and not in a terminal\n                  state (same predicate as the pie chart's\n                  overdue bucket)\n  * `completed` — item status ∈ {completed, verified}\n  * `pending`   — everything else (pending, in_progress,\n                  uploaded, submitted, scheduled, rejected,\n                  skipped)\n\nEach row carries the originating `phase` AND the row's\n`status_group` regardless of how the filter was applied, so the\nclient can render the Phase / Status columns the screenshot\nshows even when filtering on one axis.\n\nThe `responsible` block is the new hire being onboarded\n(`plan.user` or `plan.recruiting_candidate`) — NOT the task's\n`assigned_to`. The latter, when set, is surfaced separately as\n`assignee` for tasks/checkpoints.\n",
        "parameters": [
          {
            "name": "phase",
            "in": "query",
            "description": "Phase filter. One of the six standard phase keys, or `all`\nto skip phase filtering.\n",
            "schema": {
              "type": "string",
              "enum": [
                "preboarding",
                "day_1",
                "week_1",
                "month_1",
                "month_2_3",
                "beyond",
                "all"
              ],
              "default": "all"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Status filter — the canonical 3-way collapse. `all` skips\nstatus filtering.\n",
            "schema": {
              "type": "string",
              "enum": [
                "overdue",
                "completed",
                "pending",
                "all"
              ],
              "default": "all"
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Case-insensitive substring filter against item title and responsible name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated items matching the supplied filters.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "filters",
                    "items",
                    "meta"
                  ],
                  "properties": {
                    "filters": {
                      "type": "object",
                      "description": "Echo of the applied filters so the client can re-render the dropdowns.",
                      "properties": {
                        "phase": {
                          "type": "object",
                          "properties": {
                            "key": {
                              "type": "string",
                              "enum": [
                                "preboarding",
                                "day_1",
                                "week_1",
                                "month_1",
                                "month_2_3",
                                "beyond",
                                "all"
                              ],
                              "example": "all"
                            },
                            "label": {
                              "type": "string",
                              "example": "All phases"
                            }
                          }
                        },
                        "status": {
                          "type": "object",
                          "properties": {
                            "key": {
                              "type": "string",
                              "enum": [
                                "overdue",
                                "completed",
                                "pending",
                                "all"
                              ],
                              "example": "overdue"
                            },
                            "label": {
                              "type": "string",
                              "example": "Overdue"
                            }
                          }
                        }
                      }
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "type",
                          "title",
                          "status",
                          "phase",
                          "due_date",
                          "responsible",
                          "plan_id",
                          "web_url",
                          "mobile_url"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Stable composite (`<type>_<record_id>`).",
                            "example": "training_42"
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "task",
                              "form",
                              "document",
                              "checkpoint",
                              "survey",
                              "training"
                            ],
                            "example": "training"
                          },
                          "title": {
                            "type": "string",
                            "example": "Attend Aberdeen Mobilisation Briefing"
                          },
                          "status": {
                            "type": "string",
                            "description": "Raw status (with the task model's\n`display_status` collapse applied for tasks).\n",
                            "example": "pending"
                          },
                          "status_label": {
                            "type": "string",
                            "example": "Pending"
                          },
                          "status_group": {
                            "type": "string",
                            "description": "Canonical 3-way classification of the item's\ncurrent state (overdue/completed/pending) —\nsame value the `status` filter accepts.\n",
                            "enum": [
                              "overdue",
                              "completed",
                              "pending"
                            ],
                            "example": "overdue"
                          },
                          "status_color": {
                            "type": "string",
                            "description": "Bootstrap contextual color token for `status`,\nidentical to the desktop timeline badge. Prefix\nwith `bg-`/`text-` to render.\n",
                            "enum": [
                              "success",
                              "info",
                              "warning",
                              "danger",
                              "secondary"
                            ],
                            "example": "warning"
                          },
                          "icon": {
                            "type": "string",
                            "description": "FontAwesome icon class for the item type.",
                            "example": "fas fa-tasks"
                          },
                          "icon_color": {
                            "type": "string",
                            "description": "Bootstrap contextual color token for the\nitem-type `icon`.\n",
                            "example": "success"
                          },
                          "required": {
                            "type": "boolean",
                            "example": true
                          },
                          "phase": {
                            "type": "object",
                            "description": "Original phase the item belongs to — distinct\nfrom the bucket. An item in the `overdue`\nbucket still reports its source phase here.\n",
                            "properties": {
                              "key": {
                                "type": "string",
                                "enum": [
                                  "preboarding",
                                  "day_1",
                                  "week_1",
                                  "month_1",
                                  "month_2_3",
                                  "beyond"
                                ],
                                "example": "day_1"
                              },
                              "label": {
                                "type": "string",
                                "example": "Day 1"
                              }
                            }
                          },
                          "due_date": {
                            "type": "string",
                            "format": "date",
                            "nullable": true,
                            "example": "2026-04-15"
                          },
                          "days_overdue": {
                            "type": "integer",
                            "nullable": true,
                            "example": 59
                          },
                          "assignee": {
                            "type": "string",
                            "nullable": true,
                            "description": "Task/checkpoint assignee display name; nil for forms, documents, surveys, trainings.",
                            "example": null
                          },
                          "responsible": {
                            "type": "object",
                            "nullable": true,
                            "description": "The new hire being onboarded\n(`plan.user || plan.recruiting_candidate`).\nNil only when both are absent on a draft plan.\n",
                            "properties": {
                              "user_id": {
                                "type": "integer",
                                "nullable": true,
                                "example": 42
                              },
                              "candidate_id": {
                                "type": "integer",
                                "nullable": true,
                                "example": null
                              },
                              "name": {
                                "type": "string",
                                "example": "Aldin Cumpton"
                              },
                              "initials": {
                                "type": "string",
                                "example": "AC"
                              },
                              "avatar_url": {
                                "type": "string",
                                "format": "uri",
                                "nullable": true,
                                "example": "https://officechat-dev.workforce.mangoapps.com/rails/active_storage/..."
                              }
                            }
                          },
                          "plan_id": {
                            "type": "integer",
                            "example": 102
                          },
                          "web_url": {
                            "type": "string",
                            "format": "uri",
                            "description": "Absolute URL of the item's detail page in the\ndesktop Onboarding Hub — the same link the\ndesktop timeline renders. Surveys and trainings\nhave no standalone detail page, so they point at\nthe plan page.\n",
                            "example": "https://officechat-dev.workforce.mangoapps.com/apps/onboarding_hub/tasks/42"
                          },
                          "mobile_url": {
                            "type": "string",
                            "format": "uri",
                            "description": "Absolute URL of the item's detail page on the\nmobile-web surface (`/m/apps/onboarding-hub/...`).\nOnly task & document ship a dedicated mobile\ndetail page; every other type falls back to the\nmobile plan page so the link always resolves.\n",
                            "example": "https://officechat-dev.workforce.mangoapps.com/m/apps/onboarding-hub/tasks/42"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "page": {
                          "type": "integer",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "example": 20
                        },
                        "total_pages": {
                          "type": "integer",
                          "example": 2
                        },
                        "total_count": {
                          "type": "integer",
                          "example": 30
                        },
                        "phase": {
                          "type": "string",
                          "example": "all"
                        },
                        "status": {
                          "type": "string",
                          "example": "overdue"
                        },
                        "search": {
                          "type": "string",
                          "nullable": true,
                          "example": null
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unknown phase or status value.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "`invalid_phase` or `invalid_status` depending on which filter was bad.",
                          "example": "invalid_status"
                        },
                        "message": {
                          "type": "string",
                          "example": "Unknown status: foo"
                        },
                        "details": {
                          "type": "object",
                          "properties": {
                            "allowed": {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required."
          }
        }
      }
    },
    "/feeds": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List feed posts (filtered, paginated)",
        "description": "Returns the audience-scoped, published, non-expired feed posts the\ncaller can see. Mirrors the web's tabbed feed (PRD 02 §5.1) — same\nvisibility, same ordering, same filter set.\n\nDefault ordering: unread posts first, then within each\nunread/read bucket by priority (`must_read` → `operational` →\n`social`) and finally `published_at DESC`. The `pinned` filter\noverrides this with the caller's own `pinned_at DESC` (per-user);\n`scheduled` overrides with `scheduled_at ASC` (soonest first).\n\nWhen `filter=all`, the response includes a `meta.unread_counts`\nobject with per-scope unread totals (`all`, `must_read`,\n`operational`, `pinned`, `social`, `mentions`) so the client can\nrender nav badges without extra round-trips. Each bucket reuses\nthe same audience-scoped, published+active visibility constraints\nas the index list.\n\nThe `filter` and the legacy `category` / `must_read` / `segment_id`\nparams compose. `topic_id` is independent and stacks with any filter.\n",
        "parameters": [
          {
            "name": "filter",
            "in": "query",
            "description": "Primary filter. Mutually exclusive set; default `all`.\n  * `all`         — every visible published post\n  * `pinned`      — posts the calling user has personally pinned\n                    (per-user — does not include posts pinned by\n                    other users). Ordered by the caller's\n                    `pinned_at DESC`.\n  * `mentions`    — posts where the caller was @-mentioned in any\n                    comment on the post. Backed by the\n                    `news_feed_mention` notifications index — only\n                    comment mentions create notifications, so\n                    body-only mentions are NOT included here.\n  * `unread`      — visible posts that are unread for the caller RIGHT\n                    NOW (no read record, or unread again because of\n                    new comments / an owed must-read acknowledgement\n                    — the same predicate as `viewer.read == false`\n                    and `meta.unread_counts`). A post read since\n                    `as_of` was stamped is dropped from the page, so\n                    this list never contains a row whose own\n                    `viewer.read` is `true`.\n  * `must_read`   — priority='must_read'\n  * `operational` — priority='operational'\n  * `social`      — priority='social'\n  * `scheduled`   — caller's OWN posts queued for future publish\n                    (status='scheduled'). Bypasses the default\n                    published-only filter; ordered by\n                    `scheduled_at ASC` (soonest first).\n",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "pinned",
                "mentions",
                "unread",
                "must_read",
                "operational",
                "social",
                "scheduled"
              ],
              "default": "all"
            }
          },
          {
            "name": "topic_id",
            "in": "query",
            "description": "Restrict to posts assigned this topic (author or AI source). A post also matches when the topic lives on one of its active comments.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "category",
            "in": "query",
            "description": "Legacy filter by content_category. Composes with `filter`.",
            "schema": {
              "type": "string",
              "enum": [
                "operational",
                "social"
              ]
            }
          },
          {
            "name": "segment_id",
            "in": "query",
            "description": "Legacy filter — posts targeted at the given audience segment.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "must_read",
            "in": "query",
            "description": "Legacy boolean filter. Maps to `filter=must_read` if `filter` is omitted.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "as_of",
            "in": "query",
            "description": "Optional ISO-8601 read-state cutoff for the paging session. The\nunread predicate is the primary sort key of an OFFSET-paginated\nlist, so marking cards read while paging otherwise re-sorts the\nlist under the client's own offsets and page N+1 skips rows that\nslid up. Send back the `meta.as_of` value the first page returned\nand every page is ordered against the same instant.\n\nSafe to omit: the server holds the snapshot for a short paging\nsession per caller and per `filter`, so `page >= 2` reuses the window\npage 1 was ordered against even when the client sends nothing, and a\nrequest on another `filter` cannot move it. Requesting page 1 (or\nomitting `page`) always re-stamps that filter's snapshot. Values that\nare blank, unparseable, in the future, or older than 12 hours fall\nback to that server-held snapshot (never an error).\n\nORDERING ONLY. It does NOT decide what you are shown: the `unread`\nfilter's membership is resolved against live read state per page, so\na post read since the stamp is dropped from the response rather than\nlisted as unread. A page may therefore come back shorter than\n`per_page` while `meta.total_count` (the frozen window) still\ndescribes what is left to page through.\n",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated feed list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "feeds",
                    "meta"
                  ],
                  "properties": {
                    "feeds": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NewsFeedSummary"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "filter": {
                          "type": "string",
                          "example": "all"
                        },
                        "page": {
                          "type": "integer",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "example": 20
                        },
                        "total_pages": {
                          "type": "integer",
                          "example": 5
                        },
                        "total_count": {
                          "type": "integer",
                          "example": 87
                        },
                        "as_of": {
                          "type": "string",
                          "format": "date-time",
                          "description": "The read-state cutoff this page was ordered against.\nEcho it back as the `as_of` query param on page 2+ to\nhold the window still; request page 1 without it to\nstart a fresh session. Always present.\n",
                          "example": "2026-08-31T09:15:00Z"
                        },
                        "unread_counts": {
                          "type": "object",
                          "description": "Per-scope unread totals for the caller. Only\nincluded when `filter=all`. Reuses the index\nvisibility constraints — same audience, same\npublished+active filter.\n",
                          "properties": {
                            "all": {
                              "type": "integer",
                              "example": 12
                            },
                            "must_read": {
                              "type": "integer",
                              "example": 3
                            },
                            "operational": {
                              "type": "integer",
                              "example": 7
                            },
                            "pinned": {
                              "type": "integer",
                              "example": 1
                            },
                            "social": {
                              "type": "integer",
                              "example": 2
                            },
                            "mentions": {
                              "type": "integer",
                              "example": 4
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — bearer token missing or invalid"
          }
        }
      },
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Create a feed post (Update, Question, or Poll)",
        "description": "Publishes a feed post. Mirrors the web composer (PRD 02). Server-side\nguardrails:\n  * Out-of-policy content_type silently falls back to `update`\n  * Out-of-policy priority (`must_read` without permission) falls back to `operational`\n  * `priority=must_read` on a `question` or `poll` falls back to\n    `operational` — must-read is reserved for one-way operational\n    broadcasts, not interactive prompts\n  * Out-of-policy `audience_type=everyone` falls back to `segments`\n  * For `question`, the title is derived from the first non-blank line of `body` if omitted\n  * For `poll`, `poll_duration_days` (default 7, max 30) sets `closes_at`.\n    Pass the string `\"never\"` to create a poll that never auto-closes\n    (`closes_at` is null) — close it later via `POST /feeds/{id}/close_poll`.\n\nPublish-time fan-out (audience snapshot, recipient notifications,\nwebhooks, poll-close scheduling, SME routing, auto-answer) happens\nasynchronously via PublishFeedWorkflowJob — the response returns as\nsoon as the row is persisted.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "feed"
                ],
                "properties": {
                  "feed": {
                    "type": "object",
                    "properties": {
                      "content_type": {
                        "type": "string",
                        "enum": [
                          "update",
                          "question",
                          "poll"
                        ],
                        "default": "update"
                      },
                      "title": {
                        "type": "string",
                        "description": "Required for poll. Derived from body.first_line for question."
                      },
                      "body": {
                        "type": "string"
                      },
                      "priority": {
                        "type": "string",
                        "enum": [
                          "must_read",
                          "operational",
                          "social"
                        ],
                        "default": "operational"
                      },
                      "audience_type": {
                        "type": "string",
                        "enum": [
                          "everyone",
                          "segments"
                        ],
                        "default": "everyone"
                      },
                      "audience_segment_ids": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      "audience_criteria": {
                        "type": "array",
                        "description": "Structured attribute targeting — the same shapes the\nBroadcast kind accepts, so one recipient picker serves\nall four kinds of the Communications composer. Unioned\nwith `extra_user_ids` and any `audience_segment_ids`\nsent alongside, then RESOLVED at send time onto the\nfeed row (a frozen recipient list, not a stored rule —\nthe fan-out and the acknowledgement denominator are\nboth taken now).\n\nUnknown criterion types, and ids belonging to another\ntenant, are dropped server-side. An `everyone`\ncriterion absorbs the rest of the selection.\n\nA structured selection that resolves to NOBODY is a 422\n(`error.code: no_recipients`), never a silent widening\nto everyone.\n",
                        "items": {
                          "type": "object",
                          "properties": {
                            "type": {
                              "type": "string",
                              "enum": [
                                "everyone",
                                "department",
                                "job_title",
                                "location",
                                "role"
                              ]
                            },
                            "ids": {
                              "type": "array",
                              "items": {
                                "type": "integer"
                              },
                              "description": "For `department` / `location`."
                            },
                            "titles": {
                              "type": "array",
                              "items": {
                                "type": "string"
                              },
                              "description": "For `job_title`."
                            },
                            "roles": {
                              "type": "array",
                              "items": {
                                "type": "string",
                                "enum": [
                                  "admin",
                                  "manager",
                                  "member"
                                ]
                              },
                              "description": "For `role`."
                            }
                          }
                        },
                        "example": [
                          {
                            "type": "department",
                            "ids": [
                              3,
                              8
                            ]
                          },
                          {
                            "type": "role",
                            "roles": [
                              "manager"
                            ]
                          }
                        ]
                      },
                      "extra_user_ids": {
                        "type": "array",
                        "items": {
                          "type": "integer"
                        },
                        "description": "Named individuals to include, unioned with\n`audience_criteria`. Ids outside this business are\ndropped.\n"
                      },
                      "policy_id": {
                        "type": "integer",
                        "description": "The HR policy this Must-Read asks readers to accept\n(options: `GET /news-feed/policies`; one policy's\ndetail: `GET /news-feed/policies/{id}`). Honored only when\n`priority` is `must_read` — the other kinds carry no\nacknowledgement to compare an acceptance against — and\nre-scoped to this business, so a crafted id cannot\nattach another tenant's policy. Read back as the\n`policy` block on the feed payload. `hr_policy_id` is\naccepted as an alias.\n"
                      },
                      "expires_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "scheduled_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "requires_acknowledgement": {
                        "type": "boolean"
                      },
                      "ai_assisted": {
                        "type": "boolean"
                      },
                      "status": {
                        "type": "string",
                        "enum": [
                          "draft",
                          "scheduled",
                          "published"
                        ],
                        "description": "Optional. `published` is the default; `draft` saves without publishing."
                      },
                      "poll_duration_days": {
                        "description": "Days until the poll auto-closes (1-30, default 7), or the string \"never\" for a poll that stays open until closed manually (closes_at null).",
                        "default": 7,
                        "oneOf": [
                          {
                            "type": "integer",
                            "minimum": 1,
                            "maximum": 30
                          },
                          {
                            "type": "string",
                            "enum": [
                              "never"
                            ]
                          }
                        ]
                      },
                      "poll_options_attributes": {
                        "type": "array",
                        "description": "Required when content_type=poll. 2-20 options.",
                        "items": {
                          "type": "object",
                          "properties": {
                            "text": {
                              "type": "string"
                            },
                            "position": {
                              "type": "integer"
                            }
                          }
                        }
                      },
                      "poll_config_attributes": {
                        "type": "object",
                        "properties": {
                          "voting_mode": {
                            "type": "string",
                            "enum": [
                              "single",
                              "multiple"
                            ]
                          },
                          "is_anonymous": {
                            "type": "boolean"
                          },
                          "result_visibility": {
                            "type": "string",
                            "enum": [
                              "live",
                              "hidden_until_close"
                            ]
                          },
                          "allow_change_vote": {
                            "type": "boolean",
                            "default": true,
                            "description": "When false, voters cannot change their vote after submitting."
                          },
                          "allow_comments": {
                            "type": "boolean",
                            "default": true,
                            "description": "When false, the post/poll is created with comments turned off (no comment input shown, returned as `poll.allow_comments: false`). Mirrors the read shape, where this flag is surfaced inside the `poll` block. May also be sent top-level as `feed[allow_comments]`. Backed by the feed-level `comments_enabled` column, distinct from the reversible moderation discussion-close."
                          }
                        }
                      },
                      "feed_media_ids": {
                        "type": "array",
                        "items": {
                          "type": "integer"
                        },
                        "description": "Ids of FeedMedia rows pre-uploaded via `POST /media`\nand finalized via `POST /media/{id}/complete`. Each\nrow must still be orphan (not already attached) and\nowned by the caller; gallery order matches the\norder of ids in this array. Reject reasons surface\nas 422 with `error.message` describing which ids\nfailed.\n"
                      }
                    }
                  },
                  "rejected_topics": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Topic names the author dismissed in the composer suggestion strip (PRD 11)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Post created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "feed": {
                      "$ref": "#/components/schemas/NewsFeedDetail"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing `feed` parameter"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Composer disabled for this business"
          },
          "422": {
            "description": "Validation failed, or the structured audience selection\n(`audience_criteria` / `extra_user_ids`) resolved to nobody —\n`error.code: no_recipients`. The post is NOT created and the\naudience is never silently widened to everyone.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "validation_failed",
                            "no_recipients"
                          ],
                          "example": "validation_failed"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/feeds/{id}": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get a feed with the first page of top-level comments",
        "description": "Returns the feed detail payload + the first page (20) of top-level\ncomments with replies inlined. The feed payload embeds the latest\nAI discussion summary (`ai_summary`) and, for polls, the latest AI\noutcome summary (`poll_summary`) so mobile clients render the same\ncards as the web view in a single roundtrip. Both follow the\ngating rules of their web partials — see `NewsFeedDetail`.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Feed detail + embedded comments page",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "feed": {
                      "$ref": "#/components/schemas/NewsFeedDetail"
                    },
                    "comments": {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/NewsFeedComment"
                          }
                        },
                        "meta": {
                          "type": "object",
                          "properties": {
                            "current_page": {
                              "type": "integer"
                            },
                            "per_page": {
                              "type": "integer"
                            },
                            "total_count": {
                              "type": "integer"
                            },
                            "total_pages": {
                              "type": "integer"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Caller not in the feed's audience"
          },
          "404": {
            "description": "Feed not found"
          }
        }
      },
      "patch": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Edit a feed post",
        "description": "Update a feed post. Mirrors the web composer's edit semantics from\n`Apps::NewsFeed::FeedsController#update`:\n\n  * **Draft / scheduled posts** — author may update the full composer\n    param set (title, body, priority, audience, scheduling, poll\n    configuration). Submitting `status=draft` keeps it a draft; any\n    other value resolves to `published` (immediate) or `scheduled`\n    (when `scheduled_at` is in the future).\n  * **Published posts** — only `title` and `body` are accepted, and\n    the call is gated on BOTH the 15-min author edit window\n    (FR-02-17) AND the admin \"content_editing\" feature flag. Polls\n    are immutable once published (FR-02-18).\n\nAuthor-only — non-authors get 403. Outside the edit window or with\nthe admin toggle off, the call returns 403.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "feed"
                ],
                "properties": {
                  "feed": {
                    "type": "object",
                    "description": "Published posts: `{title, body}` only.\nDraft/scheduled: full composer param set (see POST /feeds).\n",
                    "properties": {
                      "title": {
                        "type": "string"
                      },
                      "body": {
                        "type": "string"
                      },
                      "priority": {
                        "type": "string",
                        "enum": [
                          "must_read",
                          "operational",
                          "social"
                        ]
                      },
                      "audience_type": {
                        "type": "string",
                        "enum": [
                          "everyone",
                          "segments"
                        ]
                      },
                      "audience_segment_ids": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      "scheduled_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                      },
                      "expires_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                      },
                      "requires_acknowledgement": {
                        "type": "boolean"
                      },
                      "status": {
                        "type": "string",
                        "enum": [
                          "draft",
                          "scheduled",
                          "published"
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated feed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "feed": {
                      "$ref": "#/components/schemas/NewsFeedDetail"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing `feed` parameter"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Caller is not the author, edit window has elapsed, the admin\ncontent-editing flag is off, or the post is a published poll.\n"
          },
          "404": {
            "description": "Feed not found"
          },
          "422": {
            "description": "Validation failure",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "validation_failed"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Delete a feed post",
        "description": "Delete a feed post. Author-only — non-authors get 403. Mirrors the\nweb composer's `#destroy`: enqueues `NewsFeed::MediaCleanupJob` to\nsweep the attached media and clears the author's draft row when\nthe deleted feed was itself a draft. Discussion closes, reactions,\ncomments, read records, and acknowledgements cascade via the\nmodel's `dependent: :destroy` associations.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Feed deleted"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Caller is not the author"
          },
          "404": {
            "description": "Feed not found"
          }
        }
      }
    },
    "/feeds/{id}/mute": {
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Mute notifications for a feed",
        "description": "Mute notifications about activity on this feed (new comments,\nreactions, etc.) for the calling user. Idempotent — repeat POSTs\nreturn the same `muted_at` timestamp. Visible-to-caller required.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Muted (or already muted)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "muted_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Feed not found"
          }
        }
      },
      "delete": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Unmute notifications for a feed",
        "description": "Remove the caller's notification mute on this feed. Idempotent —\nreturns 204 even if no mute existed.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Mute removed (or none existed)"
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Feed not found"
          }
        }
      }
    },
    "/feeds/{id}/close_discussion": {
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Close discussion on a feed",
        "description": "Close the discussion on a feed (no new comments, no new replies).\nAuthorization (PRD 04 FR-04-10): the author may close their own\nfeed's discussion, and an admin may close any feed's discussion.\nExisting comments remain visible.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Discussion closed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "discussion": {
                      "type": "object",
                      "properties": {
                        "open": {
                          "type": "boolean"
                        },
                        "closed_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "closed_by_user_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "reopened_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "reopened_by_user_id": {
                          "type": "integer",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Caller is neither author nor admin",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Feed not found"
          },
          "422": {
            "description": "Discussion is already closed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/feeds/{id}/reopen_discussion": {
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Reopen a closed discussion",
        "description": "Reopen a previously-closed discussion. Authorization (PRD 04\nFR-04-11): admin-only — authors cannot reopen their own closed\ndiscussions.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Discussion reopened",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "discussion": {
                      "type": "object",
                      "properties": {
                        "open": {
                          "type": "boolean"
                        },
                        "closed_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "closed_by_user_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "reopened_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "reopened_by_user_id": {
                          "type": "integer",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Caller is not an admin",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Feed not found"
          },
          "422": {
            "description": "Discussion is not closed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/feeds/{id}/publish": {
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Publish a scheduled feed immediately",
        "description": "Publish-now for a feed currently in `status=scheduled`. Author-only.\nMirrors the web composer's \"Publish now\" action: flips status to\n`published`, clears `scheduled_at`, stamps `published_at`, and\ntriggers the same audience snapshot + recipient fan-out workflow\n(`NewsFeed::PublishFeedWorkflowJob`).\n\nCalling on a feed that isn't in `scheduled` state returns 422.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Feed published",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "feed": {
                      "$ref": "#/components/schemas/NewsFeedDetail"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Caller is not the author"
          },
          "404": {
            "description": "Feed not found"
          },
          "422": {
            "description": "Feed is not in `scheduled` state",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "not_scheduled"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/feeds/{id}/pin": {
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Pin a feed for the calling user",
        "description": "Personal pin — the post surfaces in the calling user's `filter=pinned`\nlisting only. Pinning has no effect on any other viewer's feed. Any\nuser who can see the post can pin it; 403 is returned only when the\ncaller is not in the feed's audience.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Pinned for the calling user",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "pinned": {
                      "type": "boolean",
                      "example": true,
                      "description": "Per-user — reflects the calling user's pin state only."
                    },
                    "pinned_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "When the calling user pinned the post."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Caller is not in the feed's audience"
          },
          "404": {
            "description": "Feed not found"
          }
        }
      },
      "delete": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Unpin a feed for the calling user",
        "description": "Removes the calling user's personal pin on the post. Idempotent —\nsucceeds whether or not a pin row existed.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Unpinned for the calling user",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "pinned": {
                      "type": "boolean",
                      "example": false
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Caller is not in the feed's audience"
          },
          "404": {
            "description": "Feed not found"
          }
        }
      }
    },
    "/feeds/{id}/comments": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List comments on a feed",
        "description": "Returns the active comments page-by-page. When `parent_comment_id`\nis set, returns the replies of that comment instead of top-level\nones.\n\nEach top-level comment includes its direct replies inline under\n`replies[]` (depth-1, ordered by `created_at` ascending). Both\nthe top-level comment and each reply carry their own `media[]`\nattachments, so clients do not need a follow-up request per\ncomment to render the thread.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "parent_comment_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated comments",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "comments": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NewsFeedComment"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "current_page": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        },
                        "total_count": {
                          "type": "integer"
                        },
                        "total_pages": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Caller not in the feed's audience"
          },
          "404": {
            "description": "Feed not found"
          }
        }
      },
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Create a comment on a feed",
        "description": "Creates a top-level comment or a reply. PRD 04 Phase 16 adds optional\nattachments via `comment_media_ids[]`: mobile uploads orphan media\nthrough `POST /media` + `POST /media/{id}/complete` first, then sends\nthe resulting ids here. The server claims them inside the create\ntransaction (locked `FOR UPDATE`); any rejection — wrong owner,\nalready attached, soft-deleted, over the per-comment cap of 10 —\nrolls the whole comment back so partial attachment sets are\nimpossible. `body` may be blank when at least one attachment is\nprovided (\"attachment-only\" comments).\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "body": {
                    "type": "string",
                    "maxLength": 2000,
                    "description": "Comment text (1..2000 chars). May be blank only when\n`comment_media_ids` is non-empty.\n"
                  },
                  "parent_comment_id": {
                    "type": "integer",
                    "description": "Omit for top-level; provide to reply."
                  },
                  "ai_assisted": {
                    "type": "boolean"
                  },
                  "comment_media_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "Ids of FeedMedia rows pre-uploaded via `POST /media`\nand finalized via `POST /media/{id}/complete`. Each\nrow must still be orphan (not yet attached) and owned\nby the caller; gallery order matches the order of ids\nin this array. Cap of 10 attachments per comment.\nMirror of `feed_media_ids` on `POST /feeds`. Reject\nreasons surface as 422 with `error.message`.\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Comment created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "comment": {
                      "$ref": "#/components/schemas/NewsFeedComment"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Discussion is closed"
          },
          "404": {
            "description": "Feed not found"
          },
          "422": {
            "description": "Validation failed (body length, depth exceeded, media not owned / already claimed / cap exceeded, etc.)"
          }
        }
      }
    },
    "/feeds/{id}/reactions": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List reactors on a feed (flat + grouped-by-emoji)",
        "description": "Returns the reactors on a feed in two shapes:\n\n  * `reactors` — flat list, ordered most-recent first; used by the\n    mobile popover row.\n  * `grouped`  — same reactors bucketed by `emoji_key` and sorted\n    by count desc; used by the reactor sheet's per-emoji tabs.\n\nCapped at the most recent 200 reactors.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Flat + grouped reactor lists",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "reactors",
                    "grouped",
                    "total"
                  ],
                  "properties": {
                    "total": {
                      "type": "integer",
                      "minimum": 0
                    },
                    "reactors": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "emoji_key": {
                            "type": "string"
                          },
                          "tier": {
                            "type": "string"
                          },
                          "reacted_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "user": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string",
                                "nullable": true
                              },
                              "avatar_url": {
                                "type": "string",
                                "nullable": true,
                                "format": "uri"
                              }
                            }
                          }
                        }
                      }
                    },
                    "grouped": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "emoji_key": {
                            "type": "string"
                          },
                          "tier": {
                            "type": "string"
                          },
                          "count": {
                            "type": "integer",
                            "minimum": 0
                          },
                          "users": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "type": "integer"
                                },
                                "name": {
                                  "type": "string",
                                  "nullable": true
                                },
                                "avatar_url": {
                                  "type": "string",
                                  "nullable": true,
                                  "format": "uri"
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Caller not in the feed's audience"
          },
          "404": {
            "description": "Feed not found"
          }
        }
      },
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Add or swap the caller's reaction on a feed",
        "description": "At most one reaction per user per feed. POSTing a different emoji\nswaps in place; POSTing the same emoji is idempotent. To remove,\nsend DELETE.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emoji_key"
                ],
                "properties": {
                  "emoji_key": {
                    "type": "string",
                    "description": "One of the 14 allowed NewsFeed emoji keys (e.g., `like`, `love`, `celebrate`, `insightful`)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Aggregate reaction state after the update"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Caller not in the feed's audience"
          },
          "404": {
            "description": "Feed not found"
          },
          "422": {
            "description": "Invalid emoji_key"
          }
        }
      },
      "delete": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Remove the caller's reaction on a feed",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Reaction removed (or already absent — idempotent)"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Caller not in the feed's audience"
          },
          "404": {
            "description": "Feed not found"
          }
        }
      }
    },
    "/feeds/{id}/correct_answer": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Read the current verified answer on a Question post",
        "description": "Returns the marked correct answer or `null` when none has been\nmarked yet. Same shape as the embedded `correct_answer` block on\n`GET /feeds/{id}`, so clients can reuse one renderer.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Current correct answer (or null)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "correct_answer": {
                      "nullable": true,
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/NewsFeedCorrectAnswer"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Caller not in the feed's audience, or correct-answer marking disabled for the business"
          },
          "404": {
            "description": "Feed not found"
          }
        }
      },
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Mark a comment as the verified answer (Question posts only)",
        "description": "Only the post author or a News Feed admin may mark the correct answer.\nLocks once the discussion is closed. Replacing an existing marked\nanswer is a single transactional swap (delete + insert).\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "comment_id"
                ],
                "properties": {
                  "comment_id": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Verified answer marked",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "correct_answer": {
                      "$ref": "#/components/schemas/NewsFeedCorrectAnswer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Caller not authorized to mark the correct answer, or correct-answer marking disabled for the business"
          },
          "404": {
            "description": "Feed or comment not found"
          },
          "422": {
            "description": "Returned when the feed is not a Question, the discussion is closed,\nor the chosen comment has been deleted. `error.code` discriminates.\n"
          }
        }
      },
      "delete": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Unmark the verified answer on a Question post",
        "description": "Only the post author or a News Feed admin may unmark. Idempotent —\nreturns `correct_answer: null` even when nothing was marked.\nLocks once the discussion is closed.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Verified answer removed (or already absent — idempotent)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "correct_answer": {
                      "nullable": true,
                      "type": "object",
                      "example": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Caller not authorized to unmark, or correct-answer marking disabled for the business"
          },
          "404": {
            "description": "Feed not found"
          },
          "422": {
            "description": "Discussion is closed"
          }
        }
      }
    },
    "/feeds/{id}/mark_seen": {
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Mark a feed as read by the caller",
        "description": "Upserts a NewsFeed::ReadRecord. `read_at` is the first-view timestamp;\n`last_seen_at` is the most-recent-view timestamp (used by the \"new\nreplies since\" preview card).\n\nA must-read post logs NO view until the caller acknowledges it. While\nan acknowledgement is still owed the call is accepted but records\nnothing, and the response carries `logged: false` with\n`acknowledgement_required: true` and null timestamps. Acknowledging\nthe post logs the view; every later call then behaves normally. The\ngate lifts once the acknowledgement window has closed, since nobody\ncan acknowledge an expired must-read.\n\nPass `scroll_depth_pct` when the client can measure how much of the\npost body the reader actually saw. At >= 80 the read is promoted to a\nQUALIFIED read (PRD 18 FR-18-01, set-once) and counts toward the Read\nrate tile in post analytics; omit it and the call records a plain\nimpression, which appears in the viewers roster but not in Read rate.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "scroll_depth_pct",
            "in": "query",
            "description": "Percentage of the post body that entered the viewport, 0-100. Values >= 80 promote this read to a qualified read.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Read record persisted, or declined pending acknowledgement",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "logged": {
                      "type": "boolean",
                      "description": "False when nothing was recorded because the caller still owes an acknowledgement on a must-read post.",
                      "example": true
                    },
                    "acknowledgement_required": {
                      "type": "boolean",
                      "description": "Sent as true only when `logged` is false — the caller clears it by acknowledging the post."
                    },
                    "read_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "last_seen_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "qualified_read_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true,
                      "description": "Set once, the first time this feed was read with scroll_depth_pct >= 80. Null while the caller has only ever recorded impressions."
                    },
                    "scroll_depth_pct": {
                      "type": "number",
                      "nullable": true,
                      "description": "Scroll depth stored on the qualifying read, 0-100."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Caller not in the feed's audience"
          },
          "404": {
            "description": "Feed not found"
          }
        }
      },
      "delete": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Mark a feed as unread (remove the caller's read record)",
        "description": "Idempotent — returns 200 even when there is no read record.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Read record cleared",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "read": {
                      "type": "boolean",
                      "example": false
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Caller not in the feed's audience"
          },
          "404": {
            "description": "Feed not found"
          }
        }
      }
    },
    "/feeds/mark_all_read": {
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Mark every visible feed as read for the caller",
        "description": "Bulk-marks every audience-visible, published, non-expired feed as\nread for the caller. Inserts a NewsFeed::ReadRecord row (with\n`read_at` and `last_seen_at` set to now) for each previously\nunread feed; feeds the caller has already read are left alone.\n\nMust-read posts the caller has not acknowledged are EXCLUDED and are\nnot counted in `marked_count` — a pending acknowledgement cannot be\ncleared in bulk, only by acknowledging the post itself.\n\nIdempotent — safe to call repeatedly. Returns the number of\nfeeds newly marked so the client can update its unread badges\noptimistically.\n",
        "responses": {
          "200": {
            "description": "Bulk read receipts persisted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "marked_count",
                    "marked_at"
                  ],
                  "properties": {
                    "marked_count": {
                      "type": "integer",
                      "description": "Number of feeds newly marked as read on this call.",
                      "example": 12
                    },
                    "marked_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "Timestamp assigned to the inserted read records."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    },
    "/feeds/{id}/viewers": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List users who have viewed the feed (read receipts)",
        "description": "Read-receipt roster for a feed post. Returns the users who have\nviewed the feed (NewsFeed::ReadRecord rows), ordered\nmost-recently-viewed first and paginated. Each row carries the\ncanonical v1 user shape (id / name / avatar_url) plus the row\nsubtitle (job title / office location) and the most-recent view\ntime. Visible to anyone who can see the feed.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Items per page (default 20, max 100).",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Viewers listed (most-recently-viewed first)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "viewers",
                    "meta"
                  ],
                  "properties": {
                    "viewers": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "viewed_at"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string",
                            "nullable": true
                          },
                          "avatar_url": {
                            "type": "string",
                            "nullable": true
                          },
                          "title": {
                            "type": "string",
                            "nullable": true,
                            "description": "Viewer's job title (row subtitle — what the request calls \"position\")."
                          },
                          "location": {
                            "type": "string",
                            "nullable": true,
                            "description": "Viewer's office location (row subtitle)."
                          },
                          "viewed_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "description": "COALESCE(last_seen_at, read_at) — most-recent view time."
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "unique_view_count",
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ],
                      "properties": {
                        "unique_view_count": {
                          "type": "integer",
                          "description": "Distinct viewers (equals total_count)."
                        },
                        "total_count": {
                          "type": "integer"
                        },
                        "total_pages": {
                          "type": "integer"
                        },
                        "current_page": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Caller not in the feed's audience"
          },
          "404": {
            "description": "Feed not found"
          }
        }
      }
    },
    "/feeds/{id}/acknowledge": {
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Acknowledge a must-read feed",
        "description": "Records the caller's acknowledgement of a must-read post. Idempotent —\nrepeat acknowledgements return the original `acknowledged_at`.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Acknowledged",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "acknowledged_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Feed not found"
          },
          "422": {
            "description": "Feed is not must-read, or has expired"
          }
        }
      }
    },
    "/feeds/{id}/acknowledgements": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List the must-read compliance roster (acknowledged or pending)",
        "description": "Compliance roster for a must-read post. A single endpoint backs\nboth the \"Acknowledged\" and \"Pending\" tabs of the UI — switch\nbetween them via the `status` query param.\n\nEach row carries the canonical v1 user shape (id / name / email /\navatar_url) plus the row subtitle (job title — what callers refer\nto as \"position\" — and office location) and an `acknowledged_at`\ntimestamp (set for the `acknowledged` list, `null` for `pending`).\n\nModes (selected by `status`, default `acknowledged`):\n  * `acknowledged` — users who have recorded an acknowledgement\n    (`NewsFeed::AcknowledgementRecord` rows). Ordered\n    most-recently-acknowledged first.\n  * `pending` — users in the feed's audience (resolved via\n    `NewsFeed::AudienceResolver`) who have NOT yet acknowledged.\n    The feed author is excluded (mirrors the bulk-reminder\n    fan-out rule). Ordered by user id.\n\nAuthorization mirrors `POST /feeds/{id}/send_reminder`: only the\nfeed author, a business admin-or-above, or a `news-feed` app\nadmin may view the roster. The feed must have\n`priority='must_read'` (422 otherwise).\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Which slice of the roster to return. Default `acknowledged`.\nAny other value returns 422 `invalid_status`.\n",
            "schema": {
              "type": "string",
              "enum": [
                "acknowledged",
                "pending"
              ],
              "default": "acknowledged"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Items per page (default 20, max 100).",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Roster slice for the requested `status`. Ordering and the\nmeaning of `acknowledged_at` depend on `status` (see\ndescription).\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "users",
                    "meta"
                  ],
                  "properties": {
                    "users": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "acknowledged_at"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string",
                            "nullable": true
                          },
                          "email": {
                            "type": "string",
                            "nullable": true
                          },
                          "avatar_url": {
                            "type": "string",
                            "nullable": true
                          },
                          "title": {
                            "type": "string",
                            "nullable": true,
                            "description": "User's job title (row subtitle — what the request calls \"position\")."
                          },
                          "location": {
                            "type": "string",
                            "nullable": true,
                            "description": "User's office location (row subtitle)."
                          },
                          "acknowledged_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "description": "When the user acknowledged this must-read post.\nAlways `null` when `status=pending`.\n"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ],
                      "properties": {
                        "total_count": {
                          "type": "integer"
                        },
                        "total_pages": {
                          "type": "integer"
                        },
                        "current_page": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Caller is not author / admin / app-admin"
          },
          "404": {
            "description": "Feed not found"
          },
          "422": {
            "description": "Feed is not a must-read post, OR the `status` query param is\nnot one of `acknowledged` / `pending`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "invalid_status"
                          ]
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/feeds/{id}/send_reminder": {
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Send a manual acknowledgement reminder (bulk or single user)",
        "description": "Triggers `NewsFeed::MustReadReminderJob` (the same job the automated\nscheduler enqueues) to remind users who haven't yet acknowledged\nthis must-read post. Two modes, selected by the optional `user_id`\nfield in the request body:\n\n**Bulk mode (omit `user_id`)** — mirrors the web\n`Apps::NewsFeed::AcknowledgementsController#send_reminder` exactly:\n\n  * The feed must have `priority='must_read'` — 422 otherwise.\n  * Caller must be the feed author, a business admin-or-above, or\n    an app admin for `news-feed` — 403 otherwise.\n  * 24-hour cooldown: only one bulk reminder per feed per day,\n    tracked by `feeds.last_reminder_sent_at`. Subsequent calls\n    inside the window return 429.\n\nOn success, stamps `last_reminder_sent_at = Time.current` and\nreturns the new value as `sent_at`. Fan-out reaches every audience\nuser who hasn't already acknowledged, hasn't muted the post, and\nisn't the author.\n\n**Targeted mode (`user_id` present)** — single-recipient nudge,\nmirroring the broadcasts `POST /broadcasts/:id/remind` shape:\n\n  * Same authz gates as bulk.\n  * The target user must be in the same business AND in the feed's\n    audience (422 `not_a_recipient` otherwise).\n  * Cannot target the feed author (422 `cannot_remind_author`).\n  * Cannot target a user who has already acknowledged (422\n    `already_acknowledged`).\n  * Does **not** touch the feed-level 24h cooldown — single-user\n    reminders are independent of the bulk path.\n  * Mute is intentionally overridden — an explicit single-user\n    nudge wins over the recipient's per-post mute.\n\nOn success, returns `{ sent_at, user_id }`.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "user_id": {
                    "type": "integer",
                    "description": "Optional. When omitted, bulk reminder to every pending\naudience user. When present, targeted reminder to just\nthat user.\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reminder enqueued",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "description": "Bulk mode response",
                      "properties": {
                        "sent_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    {
                      "type": "object",
                      "description": "Targeted mode response",
                      "properties": {
                        "sent_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "user_id": {
                          "type": "integer"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Caller is not author / admin / app-admin"
          },
          "404": {
            "description": "Feed not found"
          },
          "422": {
            "description": "Feed is not must-read, OR (targeted mode) the user_id is invalid /\nnot a recipient / is the author / has already acknowledged.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "user_not_found",
                            "not_a_recipient",
                            "cannot_remind_author",
                            "already_acknowledged"
                          ]
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Bulk mode only — reminder already sent in the last 24 hours"
          }
        }
      }
    },
    "/comments/{id}": {
      "put": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Edit a comment",
        "description": "Update a comment's body. Mirrors `NewsFeed::CommentsService.edit`:\n\n  * **Author-only** — non-authors get 403.\n  * Gated on the admin \"Allow content editing\" feature flag — when\n    the flag is off, all edits return 403.\n  * Edits are restricted to the 15-minute author edit window from\n    the comment's `created_at` (FR-04-13). Expired window returns\n    422 with `error.code='edit_window_expired'`.\n  * Soft-deleted comments cannot be edited (403).\n\nOn success, sets `edited_at = Time.current` and returns the full\ncomment payload (same shape as the list/show endpoints).\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "properties": {
                  "body": {
                    "type": "string",
                    "maxLength": 2000,
                    "description": "New comment body (1..2000 chars)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated comment",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "comment": {
                      "$ref": "#/components/schemas/NewsFeedComment"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Caller is not the author, the content-editing flag is off, or\nthe comment is soft-deleted.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "not_authorized"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Comment not found"
          },
          "422": {
            "description": "Edit window expired OR validation failed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "edit_window_expired"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Soft-delete a comment",
        "description": "Soft-deletes a comment (`status='deleted'`, body cleared). Mirrors\n`NewsFeed::CommentsService.delete`:\n\n  * Allowed for the comment **author** OR any **business\n    admin-or-above** — non-authors / non-admins get 403.\n  * Writes a `NewsFeed::AuditLog` row (`delete_own_comment` or\n    `admin_delete_comment`) for moderation history.\n  * Replies remain visible underneath the now-`[deleted comment]`\n    placeholder — child comments are not cascaded.\n\nReturns 204 on success.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Comment soft-deleted"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Caller is not the author and not an admin"
          },
          "404": {
            "description": "Comment not found"
          }
        }
      }
    },
    "/feeds/{id}/close_poll": {
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Close a poll now",
        "description": "Manually close an open poll ahead of (or in the absence of) its\nscheduled `closes_at`. This is the only way to close a poll created\nwith `poll_duration_days: \"never\"`. One-way — there is no reopen, and\na closed poll's results become final and visible to everyone.\nAuthorization (PRD 05): the feed author or an admin.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Poll closed — returns the updated poll block",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "poll": {
                      "type": "object",
                      "description": "Poll state embedded on Poll-type feeds. Returned on both list rows\nand the detail endpoint. Option labels (`options[].id/text/position`)\nare included on both so cards can render the option list from the\nlist response; per-option vote counts (`options[].votes`) are\ndetail-only to keep list payloads light.\n",
                      "properties": {
                        "closes_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "voting_mode": {
                          "type": "string",
                          "nullable": true,
                          "enum": [
                            "single",
                            "multi",
                            "ranked"
                          ],
                          "description": "Controls how `my_vote_option_ids` should be interpreted:\n  * `single` — at most 1 element\n  * `multi`  — 0..N elements, order is not meaningful\n  * `ranked` — 0..N elements, ordered by rank (most-preferred first)\n"
                        },
                        "is_anonymous": {
                          "type": "boolean",
                          "nullable": true
                        },
                        "result_visibility": {
                          "type": "string",
                          "nullable": true,
                          "enum": [
                            "live",
                            "hidden_until_close"
                          ]
                        },
                        "allow_change_vote": {
                          "type": "boolean",
                          "nullable": true
                        },
                        "allow_comments": {
                          "type": "boolean",
                          "description": "Whether a comment may be posted on the poll right now\n(`Feed#comments_allowed?`). Folds the author's create-time\ncomment setting (the feed-level `comments_enabled` column) together\nwith the moderation discussion-close state, so poll cards gate their\ncomment input from one field. Set it at create/update time via\n`poll_config_attributes[allow_comments]` (or top-level\n`feed[allow_comments]`).\n"
                        },
                        "is_closed": {
                          "type": "boolean",
                          "description": "True when poll_config.closes_at has elapsed."
                        },
                        "option_count": {
                          "type": "integer",
                          "minimum": 0,
                          "description": "Number of poll options."
                        },
                        "my_vote_option_ids": {
                          "type": "array",
                          "description": "The caller's voted option_ids. Always present; empty array when\nthe caller has not voted. Interpret via `voting_mode`:\n  * `single` — at most 1 element\n  * `multi`  — 0..N elements, order not meaningful\n  * `ranked` — 0..N elements, ordered by rank (most-preferred first)\n",
                          "items": {
                            "type": "integer"
                          }
                        },
                        "results_visible": {
                          "type": "boolean",
                          "description": "Detail-only. True when the caller is allowed to see the\nper-option vote breakdown; false when results are gated.\n\nGating rules (PRD 05 FR-05-06 / FR-05-10), shared with the web\nsurface:\n  * Closed poll → true (everyone)\n  * Author or business admin → true (always)\n  * Open + `result_visibility: hidden_until_close` → false\n  * Open + `result_visibility: live`, caller has voted → true\n  * Open + `result_visibility: live`, caller has not voted → false\n\nWhen false, `total_votes` and each `options[].votes` /\n`options[].percent` are returned as `null` so clients can render\na \"results hidden\" state without inferring whether anyone has\nvoted yet.\n"
                        },
                        "total_votes": {
                          "type": "integer",
                          "minimum": 0,
                          "nullable": true,
                          "description": "Aggregate count of distinct voters (sum of active PollVote rows).\nDetail-only — returned on `GET /feeds/{id}` and on the\n`/feeds/{id}/poll_votes` responses, omitted from list rows.\n`null` when `results_visible` is false.\n"
                        },
                        "options": {
                          "type": "array",
                          "description": "Poll options. `id`, `text`, and `position` are returned on both\nlist rows and the detail endpoint. `votes` (per-option count)\nand `percent` (0–100, rounded) are detail-only — list rows omit\nthem. Both are `null` when `results_visible` is false.\n",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "text": {
                                "type": "string"
                              },
                              "position": {
                                "type": "integer"
                              },
                              "votes": {
                                "type": "integer",
                                "nullable": true,
                                "description": "Detail endpoint only. `null` when results are gated."
                              },
                              "percent": {
                                "type": "integer",
                                "minimum": 0,
                                "maximum": 100,
                                "nullable": true,
                                "description": "Detail endpoint only. Share of `total_votes` cast for this\noption, rounded to the nearest integer (0–100). `null` when\nresults are gated. Matches the value rendered by the web\n`_poll` partial so mobile and web stay in lockstep.\n"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Caller is neither author nor admin",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Feed not found"
          },
          "422": {
            "description": "Feed is not a poll, or the poll is already closed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "poll_already_closed"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/feeds/{id}/poll_votes": {
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Cast a vote on a poll feed",
        "description": "Records the caller's vote on a poll. Idempotent — if the caller has\nalready voted on this poll, returns the existing vote with\n`already_voted: true` (no new row, no counter change).\n\nFor `voting_mode: single` send `poll_option_id`. For `voting_mode:\nmulti` send `poll_option_ids` (unordered array). For `voting_mode:\nranked` send `poll_option_ids` ordered by preference (top choice\nfirst). Returns the full updated `poll` block (same shape as\n`GET /api/v1/feeds/{id}` → `poll`), including per-option vote counts,\npercentages, and `total_votes` so clients can swap the embedded poll\non a feed card without a follow-up fetch.\n\nFor `voting_mode: ranked`, each option additionally carries\n`rank_distribution` (an array of counts — voters who ranked it #1,\n#2, …), `score` (Borda points; higher = stronger preference), and\n`my_rank` (the caller's 1-based rank for that option). The `options`\narray is returned in standing order (highest `score` first), and\n`votes`/`percent` report each option's FIRST-preference tally.\n\nResult visibility (PRD 05 FR-05-06 / FR-05-10) mirrors the web\nsurface — `results_visible` reports whether the breakdown is\nunlocked for this caller. When `false`, `total_votes` and each\noption's `votes`/`percent` (and `rank_distribution`/`score` for\nranked) are returned as `null`. The vote response itself always sees\nresults in `live` mode (the caller just became a voter);\n`hidden_until_close` keeps the breakdown gated until the poll closes\nfor non-author/non-admin callers.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "poll_option_id": {
                    "type": "integer",
                    "description": "For `single` voting_mode polls."
                  },
                  "poll_option_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "For `multi` voting_mode polls (unordered) and `ranked` voting_mode polls (ordered by preference, top choice first)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Vote already recorded (idempotent replay)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/paths/~1feeds~1{id}~1poll_votes/post/responses/201/content/application~1json/schema"
                }
              }
            }
          },
          "201": {
            "description": "Vote recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "already_voted",
                    "poll"
                  ],
                  "properties": {
                    "already_voted": {
                      "type": "boolean"
                    },
                    "poll": {
                      "type": "object",
                      "description": "Poll state embedded on Poll-type feeds. Returned on both list rows\nand the detail endpoint. Option labels (`options[].id/text/position`)\nare included on both so cards can render the option list from the\nlist response; per-option vote counts (`options[].votes`) are\ndetail-only to keep list payloads light.\n",
                      "properties": {
                        "closes_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "voting_mode": {
                          "type": "string",
                          "nullable": true,
                          "enum": [
                            "single",
                            "multi",
                            "ranked"
                          ],
                          "description": "Controls how `my_vote_option_ids` should be interpreted:\n  * `single` — at most 1 element\n  * `multi`  — 0..N elements, order is not meaningful\n  * `ranked` — 0..N elements, ordered by rank (most-preferred first)\n"
                        },
                        "is_anonymous": {
                          "type": "boolean",
                          "nullable": true
                        },
                        "result_visibility": {
                          "type": "string",
                          "nullable": true,
                          "enum": [
                            "live",
                            "hidden_until_close"
                          ]
                        },
                        "allow_change_vote": {
                          "type": "boolean",
                          "nullable": true
                        },
                        "allow_comments": {
                          "type": "boolean",
                          "description": "Whether a comment may be posted on the poll right now\n(`Feed#comments_allowed?`). Folds the author's create-time\ncomment setting (the feed-level `comments_enabled` column) together\nwith the moderation discussion-close state, so poll cards gate their\ncomment input from one field. Set it at create/update time via\n`poll_config_attributes[allow_comments]` (or top-level\n`feed[allow_comments]`).\n"
                        },
                        "is_closed": {
                          "type": "boolean",
                          "description": "True when poll_config.closes_at has elapsed."
                        },
                        "option_count": {
                          "type": "integer",
                          "minimum": 0,
                          "description": "Number of poll options."
                        },
                        "my_vote_option_ids": {
                          "type": "array",
                          "description": "The caller's voted option_ids. Always present; empty array when\nthe caller has not voted. Interpret via `voting_mode`:\n  * `single` — at most 1 element\n  * `multi`  — 0..N elements, order not meaningful\n  * `ranked` — 0..N elements, ordered by rank (most-preferred first)\n",
                          "items": {
                            "type": "integer"
                          }
                        },
                        "results_visible": {
                          "type": "boolean",
                          "description": "Detail-only. True when the caller is allowed to see the\nper-option vote breakdown; false when results are gated.\n\nGating rules (PRD 05 FR-05-06 / FR-05-10), shared with the web\nsurface:\n  * Closed poll → true (everyone)\n  * Author or business admin → true (always)\n  * Open + `result_visibility: hidden_until_close` → false\n  * Open + `result_visibility: live`, caller has voted → true\n  * Open + `result_visibility: live`, caller has not voted → false\n\nWhen false, `total_votes` and each `options[].votes` /\n`options[].percent` are returned as `null` so clients can render\na \"results hidden\" state without inferring whether anyone has\nvoted yet.\n"
                        },
                        "total_votes": {
                          "type": "integer",
                          "minimum": 0,
                          "nullable": true,
                          "description": "Aggregate count of distinct voters (sum of active PollVote rows).\nDetail-only — returned on `GET /feeds/{id}` and on the\n`/feeds/{id}/poll_votes` responses, omitted from list rows.\n`null` when `results_visible` is false.\n"
                        },
                        "options": {
                          "type": "array",
                          "description": "Poll options. `id`, `text`, and `position` are returned on both\nlist rows and the detail endpoint. `votes` (per-option count)\nand `percent` (0–100, rounded) are detail-only — list rows omit\nthem. Both are `null` when `results_visible` is false.\n",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "text": {
                                "type": "string"
                              },
                              "position": {
                                "type": "integer"
                              },
                              "votes": {
                                "type": "integer",
                                "nullable": true,
                                "description": "Detail endpoint only. `null` when results are gated."
                              },
                              "percent": {
                                "type": "integer",
                                "minimum": 0,
                                "maximum": 100,
                                "nullable": true,
                                "description": "Detail endpoint only. Share of `total_votes` cast for this\noption, rounded to the nearest integer (0–100). `null` when\nresults are gated. Matches the value rendered by the web\n`_poll` partial so mobile and web stay in lockstep.\n"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Caller not in the feed's audience"
          },
          "404": {
            "description": "Feed not found"
          },
          "422": {
            "description": "Not a poll, poll closed, or invalid option selection"
          }
        }
      },
      "patch": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Change a previously cast poll vote",
        "description": "Changes the caller's vote on a poll. Gated on\n`poll_config.allow_change_vote` — returns `422` if the poll forbids\nvote changes or the caller has not yet voted. Atomic: the previous\nactive vote row is superseded and the new one is created in a single\ntransaction. The voter total is unchanged; only the per-option split\nshifts. Returns the same payload shape as POST — including\n`results_visible`, per-option `percent`, and `total_votes` under the\nsame gating rules.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "poll_option_id": {
                    "type": "integer",
                    "description": "For `single` voting_mode polls."
                  },
                  "poll_option_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "For `multi` voting_mode polls (unordered) and `ranked` voting_mode polls (ordered by preference, top choice first)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Vote changed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "already_voted",
                    "poll"
                  ],
                  "properties": {
                    "already_voted": {
                      "type": "boolean"
                    },
                    "poll": {
                      "type": "object",
                      "description": "Poll state embedded on Poll-type feeds. Returned on both list rows\nand the detail endpoint. Option labels (`options[].id/text/position`)\nare included on both so cards can render the option list from the\nlist response; per-option vote counts (`options[].votes`) are\ndetail-only to keep list payloads light.\n",
                      "properties": {
                        "closes_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "voting_mode": {
                          "type": "string",
                          "nullable": true,
                          "enum": [
                            "single",
                            "multi",
                            "ranked"
                          ],
                          "description": "Controls how `my_vote_option_ids` should be interpreted:\n  * `single` — at most 1 element\n  * `multi`  — 0..N elements, order is not meaningful\n  * `ranked` — 0..N elements, ordered by rank (most-preferred first)\n"
                        },
                        "is_anonymous": {
                          "type": "boolean",
                          "nullable": true
                        },
                        "result_visibility": {
                          "type": "string",
                          "nullable": true,
                          "enum": [
                            "live",
                            "hidden_until_close"
                          ]
                        },
                        "allow_change_vote": {
                          "type": "boolean",
                          "nullable": true
                        },
                        "allow_comments": {
                          "type": "boolean",
                          "description": "Whether a comment may be posted on the poll right now\n(`Feed#comments_allowed?`). Folds the author's create-time\ncomment setting (the feed-level `comments_enabled` column) together\nwith the moderation discussion-close state, so poll cards gate their\ncomment input from one field. Set it at create/update time via\n`poll_config_attributes[allow_comments]` (or top-level\n`feed[allow_comments]`).\n"
                        },
                        "is_closed": {
                          "type": "boolean",
                          "description": "True when poll_config.closes_at has elapsed."
                        },
                        "option_count": {
                          "type": "integer",
                          "minimum": 0,
                          "description": "Number of poll options."
                        },
                        "my_vote_option_ids": {
                          "type": "array",
                          "description": "The caller's voted option_ids. Always present; empty array when\nthe caller has not voted. Interpret via `voting_mode`:\n  * `single` — at most 1 element\n  * `multi`  — 0..N elements, order not meaningful\n  * `ranked` — 0..N elements, ordered by rank (most-preferred first)\n",
                          "items": {
                            "type": "integer"
                          }
                        },
                        "results_visible": {
                          "type": "boolean",
                          "description": "Detail-only. True when the caller is allowed to see the\nper-option vote breakdown; false when results are gated.\n\nGating rules (PRD 05 FR-05-06 / FR-05-10), shared with the web\nsurface:\n  * Closed poll → true (everyone)\n  * Author or business admin → true (always)\n  * Open + `result_visibility: hidden_until_close` → false\n  * Open + `result_visibility: live`, caller has voted → true\n  * Open + `result_visibility: live`, caller has not voted → false\n\nWhen false, `total_votes` and each `options[].votes` /\n`options[].percent` are returned as `null` so clients can render\na \"results hidden\" state without inferring whether anyone has\nvoted yet.\n"
                        },
                        "total_votes": {
                          "type": "integer",
                          "minimum": 0,
                          "nullable": true,
                          "description": "Aggregate count of distinct voters (sum of active PollVote rows).\nDetail-only — returned on `GET /feeds/{id}` and on the\n`/feeds/{id}/poll_votes` responses, omitted from list rows.\n`null` when `results_visible` is false.\n"
                        },
                        "options": {
                          "type": "array",
                          "description": "Poll options. `id`, `text`, and `position` are returned on both\nlist rows and the detail endpoint. `votes` (per-option count)\nand `percent` (0–100, rounded) are detail-only — list rows omit\nthem. Both are `null` when `results_visible` is false.\n",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "text": {
                                "type": "string"
                              },
                              "position": {
                                "type": "integer"
                              },
                              "votes": {
                                "type": "integer",
                                "nullable": true,
                                "description": "Detail endpoint only. `null` when results are gated."
                              },
                              "percent": {
                                "type": "integer",
                                "minimum": 0,
                                "maximum": 100,
                                "nullable": true,
                                "description": "Detail endpoint only. Share of `total_votes` cast for this\noption, rounded to the nearest integer (0–100). `null` when\nresults are gated. Matches the value rendered by the web\n`_poll` partial so mobile and web stay in lockstep.\n"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Caller not in the feed's audience"
          },
          "404": {
            "description": "Feed not found"
          },
          "422": {
            "description": "Not a poll, poll closed, change-vote disabled, no previous vote, or invalid option selection"
          }
        }
      }
    },
    "/feeds/{id}/poll_option_voters": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List users who voted for a single poll option",
        "description": "Drill-down roster for one poll option — the users who picked it.\nA single endpoint serves both poll shapes:\n\n  * `single` / `multi` voting modes → a flat `users` list.\n  * `ranked` voting mode → `grouped` buckets, one per rank position\n    the voters gave this option (rank 1 first), so a client can\n    render the per-rank accordion.\n\nEach row carries the canonical v1 user shape (id / name / avatar_url)\nplus the row subtitle (job title / office location) — the same shape\nas `GET /feeds/{id}/viewers`. The voter set and per-voter rank are\nread at request time from the active poll votes.\n\nAuthorization layers on top of the feed audience gate:\n  * Anonymous polls (`is_anonymous = true`) never expose voters (403).\n  * Otherwise the same visibility decider as the embedded poll block\n    applies — an open `live` poll the caller has not voted in, or an\n    open `hidden_until_close` poll viewed by a non-author/admin,\n    returns 403.\n\nThe roster is capped at 200 voters per option.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "option_id",
            "in": "query",
            "required": true,
            "description": "Poll option whose voters to list. Must belong to this feed's poll.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Voter roster for the option. `users` is present for single/multi\npolls; `grouped` is present for ranked polls.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "poll_option_id",
                    "voting_mode",
                    "total"
                  ],
                  "properties": {
                    "poll_option_id": {
                      "type": "integer"
                    },
                    "voting_mode": {
                      "type": "string",
                      "enum": [
                        "single",
                        "multi",
                        "ranked"
                      ]
                    },
                    "total": {
                      "type": "integer",
                      "description": "Number of voters returned (capped at 200)."
                    },
                    "users": {
                      "type": "array",
                      "description": "Flat voter list — present for single/multi polls.",
                      "items": {
                        "type": "object",
                        "required": [
                          "id"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string",
                            "nullable": true
                          },
                          "avatar_url": {
                            "type": "string",
                            "nullable": true
                          },
                          "title": {
                            "type": "string",
                            "nullable": true,
                            "description": "Voter's job title (row subtitle)."
                          },
                          "location": {
                            "type": "string",
                            "nullable": true,
                            "description": "Voter's office location (row subtitle)."
                          }
                        }
                      }
                    },
                    "grouped": {
                      "type": "array",
                      "description": "Per-rank buckets (rank 1 first) — present for ranked polls.",
                      "items": {
                        "type": "object",
                        "required": [
                          "rank",
                          "count",
                          "users"
                        ],
                        "properties": {
                          "rank": {
                            "type": "integer",
                            "description": "1-based rank these voters gave the option."
                          },
                          "count": {
                            "type": "integer"
                          },
                          "users": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "required": [
                                "id"
                              ],
                              "properties": {
                                "id": {
                                  "type": "integer"
                                },
                                "name": {
                                  "type": "string",
                                  "nullable": true
                                },
                                "avatar_url": {
                                  "type": "string",
                                  "nullable": true
                                },
                                "title": {
                                  "type": "string",
                                  "nullable": true
                                },
                                "location": {
                                  "type": "string",
                                  "nullable": true
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Anonymous poll",
            "or results not yet visible to the caller": null
          },
          "404": {
            "description": "Feed or poll option not found"
          },
          "422": {
            "description": "Feed is not a poll"
          }
        }
      }
    },
    "/news-feed/audiences": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List composer audiences (mobile)",
        "description": "Returns the audiences the caller may broadcast to from the composer.\nThe list always begins with the virtual \"My Direct Reports\" segment,\nfollowed by every `NotificationRecipientGroup` the caller is allowed\nto target under the active rule:\n\n  * Admin / News Feed app admin → every manageable group\n  * Mode B (app-access rules configured) → groups in rules ∩ caller's groups\n  * Mode A (no rules)                    → caller's groups only\n\nEach entry carries an approximate `member_count` so the picker can\nrender \"Engineering (42)\". `member_count` is `null` when the group\nneeds runtime context (location, shift, radius) that the bare lookup\ncan't supply — clients should hide the count in that case.\n",
        "responses": {
          "200": {
            "description": "Allowed audiences for the composer",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "audiences"
                  ],
                  "properties": {
                    "audiences": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "name",
                          "kind"
                        ],
                        "properties": {
                          "id": {
                            "oneOf": [
                              {
                                "type": "string",
                                "example": "direct_reports"
                              },
                              {
                                "type": "string",
                                "example": "17"
                              }
                            ],
                            "description": "`'direct_reports'` for the virtual segment, otherwise\nthe stringified NotificationRecipientGroup id.\n"
                          },
                          "name": {
                            "type": "string",
                            "example": "Engineering"
                          },
                          "description": {
                            "type": "string",
                            "nullable": true,
                            "example": "All engineering staff in the org",
                            "description": "Human-readable description of the audience.\n`null` for groups that don't have one configured.\nThe virtual `direct_reports` segment carries a\nfixed sentence describing the relationship.\n"
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "virtual",
                              "group"
                            ]
                          },
                          "member_count": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Approximate count; `null` when the group needs runtime context."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    },
    "/news-feed/topics": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List topic taxonomy",
        "description": "Returns the News Feed topic taxonomy for the caller's business.\nUsed by the feed filter chip strip and the composer topic picker.\nNames are normalized to lowercase-hyphen slug form.\n",
        "parameters": [
          {
            "name": "managed",
            "in": "query",
            "description": "When true, restrict to admin-curated topics.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Lifecycle filter. Defaults to `active`.",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "disabled"
              ],
              "default": "active"
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Prefix-match against the normalized name (e.g. `safe` matches `safety-update`).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Topic list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "topics",
                    "meta"
                  ],
                  "properties": {
                    "topics": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string",
                            "example": "benefits"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "active",
                              "disabled"
                            ]
                          },
                          "managed": {
                            "type": "boolean"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "count": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    },
    "/news-feed/my-posts": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List the caller's own posts (My Posts)",
        "description": "The mobile \"My Posts\" screen — the calling user's OWN communications\nacross BOTH stores (feed posts and broadcasts), grouped by lifecycle.\nFaithfully mirrors the web `Apps::NewsFeedController#my_posts`: same two\nstores, same lifecycle scopes, same `updated_at DESC` merge ordering,\nand the same acknowledgement-progress source.\n\nFilters (`filter` query param, mutually exclusive):\n  * `scheduled` — queued to publish (`status=scheduled`), EXCLUDING\n                  anything currently held at an approval gate.\n  * `approval`  — items sitting at a pending approval gate (a pending\n                  `CommsHub::ApprovalRequest`), whatever the item's own\n                  status. Alias: `awaiting_approval`.\n  * `sent`      — already out: feeds `published`/`expired`, broadcasts\n                  `published`/`archived`. Alias: `published`.\n\nEach row is projected into one kind-agnostic card shape whether it is a\nfeed or a broadcast (`kind`). Acknowledgement progress\n(`acknowledged_count` / `recipient_count` / `acknowledgement_percent`)\nis populated only on the `sent` filter and only for posts that gate on\nacknowledgement (must-read feeds and ack-required broadcasts); it is\n`null` on every other row — matching exactly where the web renders the\nack bar.\n\n`meta.filter_counts` carries the per-filter totals (scheduled /\napproval / sent) so the client can render the tab badges without extra\nround-trips.\n",
        "parameters": [
          {
            "name": "filter",
            "in": "query",
            "description": "Lifecycle filter; defaults to `sent`, matching the web My Posts\nscreen. Unrecognized values fall back to `sent`.\n",
            "schema": {
              "type": "string",
              "enum": [
                "scheduled",
                "approval",
                "awaiting_approval",
                "sent",
                "published"
              ],
              "default": "sent"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "1-indexed page number.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's posts for the requested filter",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "posts",
                    "meta"
                  ],
                  "properties": {
                    "posts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "kind",
                          "type",
                          "type_label",
                          "status",
                          "status_label",
                          "must_read",
                          "title",
                          "author"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "feed",
                              "broadcast"
                            ]
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "update",
                              "question",
                              "poll",
                              "broadcast"
                            ],
                            "description": "Machine value — feed `content_type`, or `broadcast`."
                          },
                          "type_label": {
                            "type": "string",
                            "example": "Update",
                            "description": "Display label for the type chip (Update / Question / Poll / Broadcast)."
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "draft",
                              "scheduled",
                              "published",
                              "expired",
                              "archived"
                            ],
                            "description": "The item's own persisted status."
                          },
                          "status_label": {
                            "type": "string",
                            "example": "Awaiting approval",
                            "description": "Chip label — the humanized status, or\n`Awaiting approval` on the `approval` filter.\n"
                          },
                          "must_read": {
                            "type": "boolean",
                            "description": "True for must-read feeds (broadcasts are always false)."
                          },
                          "is_critical": {
                            "type": "boolean",
                            "description": "True for critical broadcasts (feeds are always false)."
                          },
                          "requires_acknowledgement": {
                            "type": "boolean",
                            "description": "Whether the post gates on acknowledgement."
                          },
                          "title": {
                            "type": "string",
                            "description": "Broadcast title, or the feed's derived headline."
                          },
                          "summary": {
                            "type": "string",
                            "nullable": true,
                            "description": "Stripped, ≤220-char one-liner shown on the card. `null` when the body is empty."
                          },
                          "body": {
                            "type": "string",
                            "nullable": true,
                            "description": "Raw post body / broadcast description."
                          },
                          "scheduled_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "published_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "acknowledged_count": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Distinct acknowledgers. Populated only on `filter=sent`\nfor ack-gated posts; `null` otherwise.\n"
                          },
                          "recipient_count": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Addressable audience size (the ack denominator). `null` unless ack-gated on `sent`."
                          },
                          "acknowledgement_percent": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Rounded `acknowledged_count / recipient_count` percentage. `null` unless ack-gated on `sent`."
                          },
                          "topics": {
                            "type": "array",
                            "description": "Topic chips (feeds only, ≤3); empty for broadcasts.",
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "type": "integer"
                                },
                                "name": {
                                  "type": "string"
                                }
                              }
                            }
                          },
                          "author": {
                            "type": "object",
                            "description": "Always the calling user.",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string"
                              },
                              "avatar_url": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "filter",
                        "page",
                        "per_page",
                        "total_pages",
                        "total_count",
                        "filter_counts"
                      ],
                      "properties": {
                        "filter": {
                          "type": "string",
                          "enum": [
                            "scheduled",
                            "approval",
                            "sent"
                          ]
                        },
                        "page": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        },
                        "total_pages": {
                          "type": "integer"
                        },
                        "total_count": {
                          "type": "integer"
                        },
                        "filter_counts": {
                          "type": "object",
                          "description": "Per-filter totals for the tab badges.",
                          "properties": {
                            "scheduled": {
                              "type": "integer"
                            },
                            "approval": {
                              "type": "integer"
                            },
                            "sent": {
                              "type": "integer"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Communications not enabled or not accessible for this business"
          }
        }
      }
    },
    "/news-feed/settings": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Effective News Feed configuration + caller capabilities",
        "description": "Returns the merged News Feed configuration (marketplace schema\ndefaults ← business override ← admin flag override) plus a\n`capabilities` block derived from `NewsFeed::Permissions` and the\n`broadcast_channels` the caller may send a broadcast on. Mobile\nclients use this to decide which composer tabs to show, whether\nthe must-read toggle is available, which delivery channels to\ndraw, etc., without having to re-implement role rules.\n\nRead-only and callable by every authenticated user. The admin\nwrite surface lives at `/api/v1/admin/news-feed/settings`.\n",
        "responses": {
          "200": {
            "description": "Effective settings + caller capabilities",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "settings",
                    "capabilities",
                    "broadcast_channels"
                  ],
                  "properties": {
                    "settings": {
                      "type": "object",
                      "additionalProperties": true,
                      "description": "Free-form bag — keys come from the marketplace app's\nconfiguration_schema. Common keys include\n`composer_update_type`, `composer_question_type`,\n`composer_poll_type`, `audience_post_to_everyone`,\n`must_read_permission`.\n"
                    },
                    "capabilities": {
                      "type": "object",
                      "properties": {
                        "can_compose_any": {
                          "type": "boolean"
                        },
                        "can_post_update": {
                          "type": "boolean"
                        },
                        "can_post_question": {
                          "type": "boolean"
                        },
                        "can_post_poll": {
                          "type": "boolean"
                        },
                        "can_post_must_read": {
                          "type": "boolean"
                        },
                        "can_post_to_everyone": {
                          "type": "boolean"
                        },
                        "can_post_announcement": {
                          "type": "boolean",
                          "description": "Whether the caller may send the Announcement kind of\nthe unified Communications composer. Rides the same\ncommunicator grant as Must-read (the two differ in\nwhat the READER owes, not in who may send), so it\ntracks `can_post_must_read`.\n"
                        },
                        "can_post_broadcast": {
                          "type": "boolean",
                          "description": "Whether the caller may send the Broadcast kind. A\nDIFFERENT grant from the feed kinds: the broadcast\nengine must be entitled for the tenant (the\n`broadcast` OR `communications` app) AND the caller\nmust hold `create` on broadcasts. This is what lets\nthe compose menu express \"may announce but may not\nbroadcast\" — deriving it from `can_post_must_read`\noffers a send the server refuses.\n"
                        }
                      }
                    },
                    "broadcast_channels": {
                      "type": "array",
                      "description": "The channels a broadcast can be sent on in this tenant —\nthe Channels card of the web composer's Break-through\npanel, answered as data so a client draws the same\ncontrols instead of hardcoding a set that can disagree\nwith the tenant. Listed in composer order.\n\nSame conditions as that page:\n\n* `in_app` is always on and has no toggle\n  (`selectable: false`, no `param`).\n* `email` / `sms` / `voice` post back inside\n  `broadcast[channels][]` on `POST /api/v1/broadcasts`.\n* `signage` (\"Break-room screens\") is listed ONLY when\n  Digital Signage is entitled for the tenant AND at least\n  one active screen is registered — an entitled app with\n  no live screen is a control that cannot deliver, so the\n  entry is absent rather than present-and-useless.\n\nAlways present, for every caller — this describes the\nTENANT, not the caller. WHO may broadcast is the separate\n`capabilities.can_post_broadcast` flag: gate the compose\nentry point on that, then use this list to draw the\ncontrols. The array is never empty (`in_app` always\ndelivers).\n",
                      "items": {
                        "type": "object",
                        "required": [
                          "key",
                          "label",
                          "selectable",
                          "default_selected"
                        ],
                        "properties": {
                          "key": {
                            "type": "string",
                            "enum": [
                              "in_app",
                              "email",
                              "sms",
                              "voice",
                              "signage"
                            ]
                          },
                          "label": {
                            "type": "string",
                            "description": "Composer wording, e.g. `Voice call`, `Break-room screens`."
                          },
                          "selectable": {
                            "type": "boolean",
                            "description": "`false` only for `in_app`, which always delivers and\ncannot be switched off.\n"
                          },
                          "default_selected": {
                            "type": "boolean",
                            "description": "The composer's pre-ticked state (Email on, SMS and\nVoice off). A COMPOSER default, not a server one:\n`POST /broadcasts` reads an ABSENT `channels` key as\n\"the author didn't touch channels\" and leaves every\nchannel on, so a client rendering these controls\nmust send the full array it ends up with.\n"
                          },
                          "param": {
                            "type": "string",
                            "nullable": true,
                            "description": "Where this channel's value goes in the\n`POST /api/v1/broadcasts` body: `channels[]` for the\nper-user delivery channels, `publish_to_signage` for\nbreak-room screens (location-bound, so deliberately\nNOT folded into `channels[]`). `null` for `in_app`,\nwhich has nothing to send.\n"
                          },
                          "hint": {
                            "type": "string",
                            "nullable": true,
                            "description": "The composer's tooltip for this channel, when it has one."
                          },
                          "location_ids_param": {
                            "type": "string",
                            "description": "`signage` only — `signage_location_ids[]`, the\noptional site narrowing. Omit or send empty to show\non every screen.\n"
                          },
                          "screen_count": {
                            "type": "integer",
                            "description": "`signage` only — active screens registered for the tenant."
                          },
                          "delivery_note": {
                            "type": "string",
                            "description": "`signage` only. Screens PULL: each shows the post at\nits next refresh, and only where its own signage\nadmin has Communications posts switched on.\n"
                          },
                          "locations": {
                            "type": "array",
                            "description": "`signage` only — the sites that actually have an\nactive screen (sites without one are omitted, since\npicking them would change nothing).\n",
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "type": "integer"
                                },
                                "name": {
                                  "type": "string"
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    },
    "/news-feed/policies": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Policies a Must-Read can ask readers to accept (composer picker)",
        "description": "The options behind the composer's \"Linked policy\" dropdown. Send the\nchosen `id` back as `feed[policy_id]` on `POST /feeds`; the feed\npayload then returns it — with the reader's acceptance state — in the\n`policy` block.\n\nOnly PUBLISHED policies are listed (a draft cannot be accepted), and\nthe list is empty unless the tenant is entitled to Policy Hub, which is\nwhere reading and accepting happen. Same list, same rules as the web\ncomposer's picker (both go through `Comms::LinkablePolicies`).\n\nA lean row per option — one policy's full detail (type, version,\nacceptance state, e-signature requirement) is\n`GET /news-feed/policies/{id}`.\n\nComposer-gated: a caller who can neither compose nor post a must-read\ngets 403.\n",
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Narrows by title (the picker's type-ahead)."
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 25,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Linkable policies",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "policies",
                    "meta"
                  ],
                  "properties": {
                    "policies": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "title": {
                            "type": "string"
                          },
                          "requires_acknowledgment": {
                            "type": "boolean",
                            "description": "Whether accepting is even asked for. A policy that\nrequires no acknowledgment is still linkable (it\nnames what the must-read is about), but the card\nshould not promise an accept action for it.\n"
                          },
                          "url": {
                            "type": "string",
                            "nullable": true,
                            "description": "Absolute URL of the mobile Policy Hub screen — the\none surface that both reads and acknowledges. Null\nwhen Policy Hub is not reachable by THIS caller, so\na client never renders a link that only bounces.\n"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "total_count": {
                          "type": "integer"
                        },
                        "total_pages": {
                          "type": "integer"
                        },
                        "current_page": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "policies": [
                    {
                      "id": 12,
                      "title": "Heat Safety Procedure v4",
                      "requires_acknowledgment": true,
                      "url": "https://acme.workforce.mangoapps.com/m/apps/policy-hub/policies/12"
                    }
                  ],
                  "meta": {
                    "total_count": 1,
                    "total_pages": 1,
                    "current_page": 1,
                    "per_page": 25
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Composing isn't enabled for this account"
          }
        }
      }
    },
    "/news-feed/policies/{id}": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Details of one policy a Must-Read can ask readers to accept",
        "description": "Resolves a policy id the client already holds — from the `policy` block\non a feed payload, from a saved draft, or from\n`GET /policy_hub/policies` — into the fields a composer's \"Linked\npolicy\" field and a Must-Read card need to render. Send the `id` back as\n`feed[policy_id]` on `POST /feeds`; the feed payload then returns it,\nwith the reader's acceptance state, in its `policy` block.\n\nNot a catalogue: enumerating and searching policies is\n`GET /policy_hub/policies`. Not the accept action either — the URL that\naccepts a policy in place is `acknowledge_url` on the `policy` block of\na feed payload, which is where a Must-Read card gets it.\n\n404 for an id outside this business, for a DRAFT (never linkable — a\ndraft cannot be accepted), and for every id when the tenant is not\nentitled to Policy Hub, which is where reading and accepting happen\n(the same entitlement rule the web composer's policy field applies —\nboth go through `Comms::LinkablePolicies`). An ARCHIVED or RETIRED\npolicy IS returned: a Must-Read published while it was live still names\nit, and its title is needed to render that card — `status` says it is no\nlonger current and `url` goes null.\n\nComposer-gated: a caller who can neither compose nor post a must-read\ngets 403.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Policy detail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "policy"
                  ],
                  "properties": {
                    "policy": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "title": {
                          "type": "string"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true
                        },
                        "policy_type": {
                          "type": "string"
                        },
                        "category": {
                          "type": "string",
                          "nullable": true
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "published",
                            "archived",
                            "retired"
                          ],
                          "description": "`draft` is never returned (it 404s). A non-`published`\nvalue means the policy is no longer current: keep\nrendering the title on an existing Must-Read, but do\nnot offer it for a new one.\n"
                        },
                        "version_number": {
                          "type": "integer"
                        },
                        "requires_acknowledgment": {
                          "type": "boolean",
                          "description": "Whether accepting is even asked for. A policy that\nrequires no acknowledgment is still linkable (it names\nwhat the must-read is about), but the card should not\npromise an accept action for it.\n"
                        },
                        "requires_esignature": {
                          "type": "boolean",
                          "description": "E-signature policies cannot be click-accepted from a\nnative client — the signing flow lives on the web app,\nand `POST /policy_hub/policies/{id}/acknowledge`\nrefuses them.\n"
                        },
                        "published_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "accepted": {
                          "type": "boolean",
                          "description": "Whether THIS caller has accepted the CURRENT version.\nFalse for a caller whose acceptance HR has flagged for\nre-acknowledgment, even though the underlying record\nstays acknowledged — those are exactly the people\nPolicy Hub is chasing to re-accept.\n"
                        },
                        "accepted_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Null unless `accepted` is true."
                        },
                        "url": {
                          "type": "string",
                          "nullable": true,
                          "description": "Absolute URL of the mobile Policy Hub screen — the one\nsurface that both reads and acknowledges. Null unless\nthe policy is still published AND Policy Hub is\nreachable by THIS caller, so a client never renders a\nlink that only bounces.\n"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "policy": {
                    "id": 12,
                    "title": "Heat Safety Procedure v4",
                    "description": "What to do above 95°F.",
                    "policy_type": "safety",
                    "category": "Safety",
                    "status": "published",
                    "version_number": 4,
                    "requires_acknowledgment": true,
                    "requires_esignature": false,
                    "published_at": "2026-08-01T14:02:00Z",
                    "updated_at": "2026-08-06T09:15:00Z",
                    "accepted": false,
                    "accepted_at": null,
                    "url": "https://acme.workforce.mangoapps.com/m/apps/policy-hub/policies/12"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Composing isn't enabled for this account"
          },
          "404": {
            "description": "No such policy in this business (or a draft",
            "or Policy Hub not entitled)": null
          }
        }
      }
    },
    "/media": {
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Upload a News Feed attachment ahead of the post (orphan media)",
        "description": "Mobile/API composer flow. Returns a presigned S3 PUT URL plus a\nFeedMedia row in \"orphan\" state (`feed_id` is null). The client\nthen PUTs the bytes directly to S3 and finalizes via\n`POST /media/{id}/complete`. Finally the post is created with\n`POST /feeds` and `feed_media_ids: [<id>, ...]` — which atomically\nclaims the orphans and attaches them to the new feed.\n\nValidation mirrors the nested per-feed endpoint: 10 in-flight\nattachments per user, per-type size caps (100MB image/gif,\n500MB video, 50MB file), MIME and extension blocklists, GIF /\nmedia admin flags.\n\nOrphan rows that are never claimed are swept after 24h by\n`NewsFeed::PurgeOrphanFeedMediaJob`.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "media_type",
                  "mime_type",
                  "original_filename",
                  "file_size_bytes"
                ],
                "properties": {
                  "media_type": {
                    "type": "string",
                    "enum": [
                      "image",
                      "gif",
                      "video",
                      "file"
                    ]
                  },
                  "mime_type": {
                    "type": "string"
                  },
                  "original_filename": {
                    "type": "string"
                  },
                  "file_size_bytes": {
                    "type": "integer",
                    "minimum": 1
                  },
                  "alt_text": {
                    "type": "string"
                  },
                  "width_px": {
                    "type": "integer",
                    "description": "Image/gif natural width — skips server-side Vips decode if supplied."
                  },
                  "height_px": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Orphan media row created; client should PUT bytes to upload_url, then POST /media/{id}/complete.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "media": {
                      "type": "object",
                      "description": "Serialized FeedMedia."
                    },
                    "upload_url": {
                      "type": "string",
                      "description": "Presigned S3 PUT URL."
                    },
                    "storage_key": {
                      "type": "string"
                    },
                    "expires_in": {
                      "type": "integer",
                      "description": "Presigned URL TTL in seconds."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "422": {
            "description": "Validation failed (mime/size/cap/admin-flag).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/media/{id}/complete": {
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Finalize an orphan media upload after the S3 PUT",
        "description": "Marks the orphan FeedMedia row as ready. For images/gifs, kicks\noff dimension extraction if `width_px`/`height_px` weren't sent on\ncreate. For videos, enqueues the MediaConvert transcode job and\n(optionally) the auto-subtitle pipeline. For files, flips status\nto `ready` immediately.\n\nOnly the uploader can finalize their own orphan rows.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Updated media row.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "media": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Media not found (already claimed, deleted, or owned by another user)."
          }
        }
      }
    },
    "/inspections/inspections": {
      "get": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List inspections (personal or team)",
        "description": "Returns a paginated list of inspections, scoped role-aware:\n\n- **Default** (no `team` param or caller is not a manager): inspections\n  the caller is the inspector for. Mirrors the desktop\n  `/apps/inspections/inspections/my_inspections` action.\n- **`?team=true`** AND caller is `manager_or_above?` or app-admin for\n  `inspections`: the team feed honoring the tenant's\n  `team_inspections_scope` setting (location / department / none) via\n  `Inspections::TeamScopeService`.\n\nA caller who passes `?team=true` but lacks manager / inspections-admin\npermission receives **403 Forbidden** — the request is rejected rather\nthan silently downgraded to the personal feed, so clients learn they\nlack team visibility.\n\n**Ordering** depends on `status_type` (each value mirrors the\ncorresponding desktop bucket action so the API and the web list agree):\n- `status_type=overdue` — earliest `due_at` first (most overdue at the\n  top). Returns scheduled/in_progress inspections whose `due_at` is in\n  the past.\n- `status_type=active` — **urgency order** on the personal feed:\n  Overdue first, then In Progress, then Scheduled; within each group the\n  soonest `due_at` first (NULLs last), then `created_at DESC` and `id\n  DESC` as tiebreaks — a total order, so paging is stable and a row\n  cannot repeat on one page and be skipped on the next. Returns\n  inspections with `status IN (scheduled,\n  in_progress)` — the rows counted by the personal-feed Active tab badge\n  (`segment_counts.active` in this same response). Excludes `draft`\n  (training-gated and not yet startable — reachable via the unfiltered\n  list), `in_review` and `cancelled`. On the team feed (`?team=true`)\n  `active` is not a tab and sorts by newest `created_at` first.\n- `status_type=in_progress` — most recently started first\n  (`started_at DESC`, NULLs last).\n- `status_type=scheduled` — earliest scheduled first\n  (`scheduled_at ASC`, NULLs last).\n- `status_type=in_review` — earliest `due_at` first (NULLs last).\n- `status_type=completed` — most recently completed first\n  (`completed_at DESC`, NULLs last). Superset — includes passed,\n  failed, and outcome-less rows.\n- `status_type=failed` — most recently completed first\n  (`completed_at DESC`, NULLs last). Returns completed inspections\n  that did not pass (`passed IS NOT TRUE` — i.e. `passed = false`\n  OR `passed IS NULL`, matching the UI's \"Failed\" badge).\n- `status_type=passed` — most recently completed first\n  (`completed_at DESC`, NULLs last). Returns completed inspections\n  with `passed = TRUE` (strict — excludes outcome-less rows).\n- No `status_type` filter — ordering depends on the feed:\n  - **Personal feed** (`team` omitted / `false`): the same **urgency\n    order** as `status_type=active`, because the personal feed is a work\n    queue and the most urgent work must land on page 1 rather than\n    wherever `created_at` happens to put it. Every status is still\n    returned (no filter is applied); `draft`, `in_review`, `completed`\n    and `cancelled` simply sort after all actionable rows. Clients that\n    narrow this list themselves therefore get overdue work first without\n    having to send `status_type` or re-sort the page.\n  - **Team feed** (`?team=true`): newest `created_at` first, matching\n    the desktop \"All\" inspections page.\n",
        "parameters": [
          {
            "name": "team",
            "in": "query",
            "description": "Set to `true` to request the team feed. Requires manager-or-above\nor inspections app-admin permission; unauthorized callers get a\n403. Omit or set to `false` for the personal feed.\n",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "status_type",
            "in": "query",
            "description": "Semantic status bucket. Also drives result ordering (see endpoint\ndescription). Omit for all statuses.\n`overdue`     = scheduled/in_progress with due_at < now.\n`active`      = status IN (scheduled, in_progress) — matches the\n                personal-feed Active tab badge\n                (`segment_counts.active`). Excludes `draft`\n                (training-gated, not yet startable).\n`in_progress` = status=in_progress.\n`scheduled`   = status=scheduled.\n`in_review`   = status=in_review.\n`completed`   = status=completed (includes passed + failed +\n                outcome-less).\n`failed`      = status=completed AND `passed IS NOT TRUE`\n                (matches the UI's \"Failed\" badge).\n`passed`      = status=completed AND `passed = TRUE` (strict).\n",
            "schema": {
              "type": "string",
              "enum": [
                "overdue",
                "active",
                "in_progress",
                "scheduled",
                "in_review",
                "completed",
                "failed",
                "passed"
              ]
            }
          },
          {
            "name": "type",
            "in": "query",
            "description": "Filter by inspection template category (case-insensitive).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "template_id",
            "in": "query",
            "description": "Filter by exact inspection_template_id.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "location_id",
            "in": "query",
            "description": "Team feed only. Restrict the team feed to a single location. Honored\nonly when `team=true`, the tenant's `team_inspections_scope` is\n`location`, and the id is one the caller may filter by (see the\n`location_filter.options` block in the response — ids outside that\nset are ignored and the default \"all my locations\" scope is returned).\n",
            "schema": {
              "type": "integer"
            }
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "List of inspections (ordering depends on `status_type` — see description)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/InspectionSummary"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PaginationMeta"
                    },
                    "location_filter": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/InspectionLocationFilter"
                        }
                      ],
                      "description": "Present only for the team feed (`team=true`). The\npermission-scoped set of locations the caller may filter\nby, for the location dropdown. Absent on the personal feed.\n"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — bearer token missing or invalid"
          },
          "403": {
            "description": "Forbidden — Inspections app disabled, no access, or `team=true` without manager/admin permission"
          }
        }
      },
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Create an inspection",
        "description": "Creates a new inspection in `in_progress` state, with the caller as\nthe inspector. Optionally accepts a batch of item updates and a\n`complete: true` flag for one-shot offline-drafted submissions —\nthe inspection transitions to `completed` (or `in_review` if the\ntemplate has an approval workflow) before responding.\n\nPass an `Idempotency-Key` header so a retried POST returns the\noriginal response instead of creating a duplicate inspection.\n",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Client-generated UUID; retries with the same key return the original response.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InspectionCreateRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Inspection created (and optionally submitted)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "inspection": {
                      "$ref": "#/components/schemas/InspectionDetail"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Template not found or inactive"
          },
          "422": {
            "description": "Validation failed"
          }
        }
      }
    },
    "/inspections/inspections/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Inspection ID",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Fetch a single inspection",
        "description": "Returns the full inspection payload — every item with its compliance\nstatus, attached `media_items` (photos + videos, polymorphic\n`MediaItem` rows), GPS, approval state, corrective actions, and\nmetadata. (Replaces the legacy `photos: []` / `videos: []` keys;\nitems now carry `media_items: [...]`, `photo_count`, `video_count`,\nand `media_count`.)\n",
        "responses": {
          "200": {
            "description": "Inspection detail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "inspection": {
                      "$ref": "#/components/schemas/InspectionDetail"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Inspection not found"
          }
        }
      },
      "patch": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Update an inspection",
        "description": "Mirrors the desktop Edit Inspection form\n(`/apps/inspections/inspections/:id/edit`). The body's allowlist is the\nsame set the web form exposes — `title`, `inspection_template_id`,\n`location_id`, `scheduled_at`, `due_at`, and `notes`. Fields outside\nthe allowlist are silently dropped.\n\n**Template-change guard** (web parity): the desktop form disables the\ntemplate select once the inspection is `in_progress` or `completed`.\nThis endpoint enforces the same rule — submitting a different\n`inspection_template_id` in those states returns\n`422 template_locked`. Other states (draft, scheduled, in_review,\ncancelled) leave the field editable.\n\nAuthority: the assigned inspector OR a manager / inspections-admin.\nMembers cannot edit colleagues' inspections (403).\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InspectionUpdateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Inspection updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "inspection": {
                      "$ref": "#/components/schemas/InspectionDetail"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — caller lacks modify permission"
          },
          "404": {
            "description": "Inspection not found"
          },
          "422": {
            "description": "Validation failed, invalid_template, invalid_location, or template_locked"
          }
        }
      },
      "delete": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Delete an inspection",
        "description": "Removes the inspection (mirrors the desktop dropdown's Delete action).\nAuthority matches the web: the assigned inspector OR a manager /\ninspections-admin. Members cannot delete colleagues' inspections.\n",
        "responses": {
          "204": {
            "description": "Inspection deleted"
          },
          "403": {
            "description": "Forbidden — caller lacks modify permission"
          },
          "404": {
            "description": "Inspection not found"
          },
          "422": {
            "description": "Inspection could not be deleted (callback / FK constraint)"
          }
        }
      }
    },
    "/inspections/inspections/{id}/start": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Transition draft/scheduled → in_progress",
        "description": "Only the assigned inspector can start an inspection. No-op if the\ninspection is already `in_progress` (returns 200 with the current\nstate).\n",
        "responses": {
          "200": {
            "description": "Inspection started",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "inspection": {
                      "$ref": "#/components/schemas/InspectionDetail"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — caller is not the inspector"
          },
          "422": {
            "description": "Inspection is not in a startable state"
          }
        }
      }
    },
    "/inspections/inspections/{id}/complete": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Submit an inspection for review or final completion",
        "description": "Transitions the inspection to `completed` (templates without an\napproval workflow) or `in_review` (templates with an approval\nworkflow). Accepts an optional base64-encoded PNG signature and\ninspector notes, plus an optional `corrective_actions` array to\ncreate or update follow-up actions atomically as part of the same\ncompletion (rolled back together with the state transition if any\nentry fails validation).\n",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InspectionCompleteRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Inspection completed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "inspection": {
                      "$ref": "#/components/schemas/InspectionDetail"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — caller is not the inspector"
          },
          "422": {
            "description": "Inspection is not in a completable state, or `items_pending` —\none or more items that block completion are still unanswered.\nConditionally-hidden items (unmet `show_when`) and visible items\nwhose `require_when` isn't currently satisfied are excluded from\nthis gate; a visible item whose `require_when` IS satisfied must\nbe answered. Details carry `pending_count`.\n"
          }
        }
      }
    },
    "/inspections/inspections/{id}/cancel": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Cancel an in-flight inspection",
        "description": "Marks the inspection as `cancelled`. Only the assigned inspector\n(or a manager) can cancel. A reason is recommended for the audit\nlog but not enforced at the API layer.\n",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InspectionCancelRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Inspection cancelled",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "inspection": {
                      "$ref": "#/components/schemas/InspectionDetail"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — caller cannot cancel this inspection"
          },
          "422": {
            "description": "Inspection is already completed"
          }
        }
      }
    },
    "/inspections/inspections/{id}/sync": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Batched draft save / offline sync for an in-flight inspection",
        "description": "Applies a batch of item updates + inspection-level notes/GPS to an\nin-progress inspection without transitioning state. Rejects writes\nwhen the inspection is completed, cancelled, or in_review (409).\nPass `complete: true` to optionally call `submit!` after applying\nupdates. Used by the native client's Save button (draft persistence)\nand by the offline-sync flow when buffered edits are flushed.\n\nAlso accepts a `corrective_actions` array to create / update\nfollow-up actions atomically alongside the item writes — invalid\nentries (cross-tenant `assigned_to_id`, off-inspection\n`inspection_item_id`, bad priority / status) roll back the entire\nsync (422) rather than producing partial writes.\n",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InspectionSyncRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Inspection updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "inspection": {
                      "$ref": "#/components/schemas/InspectionDetail"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — caller is not the inspector"
          },
          "409": {
            "description": "Inspection is locked (completed / cancelled / in_review)"
          },
          "422": {
            "description": "Validation failure on one of the items, or (with `complete: true`)\n`items_pending` — see the complete endpoint; conditionally-hidden\nand not-currently-required items don't block.\n"
          }
        }
      }
    },
    "/inspections/templates": {
      "get": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List inspection templates",
        "description": "Returns paginated inspection templates available to the caller's\nbusiness. By default only active templates are returned; include\nsystem-wide templates (business_id IS NULL) with `include_system=true`.\n",
        "parameters": [
          {
            "name": "active_only",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": true
            },
            "description": "Restrict to active templates."
          },
          {
            "name": "category",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Exact category match (e.g. `safety`, `quality`, `equipment`)."
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Case-insensitive search across name and description."
          },
          {
            "name": "include_system",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "Include system templates (business_id IS NULL)."
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "Templates list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/InspectionTemplateSummary"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PaginationMeta"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/inspections/templates/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Fetch a template with full item definitions",
        "description": "Returns the full template payload — every item the inspector will\nbe asked to answer, with item-type metadata, options, min/max\nbounds, and the failure-prompt settings the form renderer needs.\n",
        "responses": {
          "200": {
            "description": "Template detail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "template": {
                      "$ref": "#/components/schemas/InspectionTemplateDetail"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Template not found"
          }
        }
      }
    },
    "/inspections/corrective_actions": {
      "get": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List corrective actions",
        "description": "Returns paginated corrective actions. Default scope is \"assigned to\nme\"; managers can opt into the team scope via `?team=true`.\n\nThe personal (\"assigned to me\") feed defaults to **open** actions\n(`pending` + `in_progress`) when no `status`/`overdue` filter is\ngiven — mirroring the desktop My Actions list. This keeps a completed\naction off the Active tab once it has been marked complete. Pass an\nexplicit `status` (including `status=completed` for the Completed tab,\nor `status=all` for every status) to opt out of that default. The team\nscope is never narrowed this way.\n",
        "parameters": [
          {
            "name": "assigned_to_me",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": true
            },
            "description": "When false, also surfaces actions the caller created (non-manager\nfallback). Ignored when `team=true` resolves to manager scope.\n"
          },
          {
            "name": "for_inspection_id",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Restrict to actions on a specific inspection."
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "in_progress",
                "completed",
                "cancelled",
                "open",
                "closed",
                "all"
              ]
            },
            "description": "Raw enum (`pending`/`in_progress`/`completed`/`cancelled`) or a\nsemantic segment: `open` (pending + in_progress — the Active tab),\n`closed` (completed + cancelled), or `all` (every status, opts out\nof the personal open-by-default).\n"
          },
          {
            "name": "overdue",
            "in": "query",
            "schema": {
              "type": "boolean"
            },
            "description": "Only open actions past their due_date."
          },
          {
            "name": "team",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "Manager-only. Widens the scope across the team. Non-managers\nwho pass `team=true` silently get the personal scope.\n"
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "Corrective actions list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/InspectionCorrectiveAction"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PaginationMeta"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/inspections/corrective_actions/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Fetch a single corrective action",
        "responses": {
          "200": {
            "description": "Corrective action detail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "corrective_action": {
                      "$ref": "#/components/schemas/InspectionCorrectiveAction"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found"
          }
        }
      },
      "patch": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Update a corrective action",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InspectionCorrectiveActionUpdateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "corrective_action": {
                      "$ref": "#/components/schemas/InspectionCorrectiveAction"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          },
          "422": {
            "description": "Validation failed"
          }
        }
      }
    },
    "/inspections/corrective_actions/{id}/complete": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Mark a corrective action complete",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "resolution_notes": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Completed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "corrective_action": {
                      "$ref": "#/components/schemas/InspectionCorrectiveAction"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not found"
          },
          "422": {
            "description": "Already closed"
          }
        }
      }
    },
    "/inspections/direct_uploads": {
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Request a signed direct-upload URL for an inspection photo or video",
        "description": "Wraps Active Storage's direct-upload endpoint with API-style auth\nand JSON envelope. The client SHA256-hashes its bytes, calls this\nendpoint to get back a presigned S3 URL plus the required headers,\nPUTs the raw bytes directly to S3, then passes the returned\n`signed_id` back to `POST inspections/:id/items/:id/media` (or the\n`inspections#create` / `inspections#sync` `media_uploads` array)\nto attach the blob.\n\nLimits: photos ≤ 20 MB, videos ≤ 50 MB. Allowed content types are\nlisted in `DirectUploadBlobRequest.content_type`. To upload a\nnon-media file (e.g. a PDF or document), set the top-level `non_media`\nflag to true — that bypasses the content-type allow-list (the size cap\nstill applies, ≤ 50 MB for non-photo types).\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DirectUploadBlobRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Signed upload URL issued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "blob": {
                      "$ref": "#/components/schemas/DirectUploadBlobResponse"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Invalid content_type or file too large"
          }
        }
      }
    },
    "/inspections/inspections/awaiting_my_review": {
      "get": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Inspections in_review that the caller can approve",
        "description": "Returns inspections currently in `in_review` where the calling user\nis eligible to approve or reject at the current approval level.\nBacked by `ApprovalRequest.pending` filtered by\n`current_approval_level.can_approve?`. Capped at 50 rows — managers\nwith deeper queues should also call the team list with\n`?status=in_review`.\n",
        "responses": {
          "200": {
            "description": "Pending review items",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/InspectionSummary"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — Inspections app disabled or no access"
          }
        }
      }
    },
    "/inspections/inspections/{id}/bundle": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "If-None-Match",
          "in": "header",
          "required": false,
          "schema": {
            "type": "string"
          },
          "description": "ETag from a prior bundle response; returns 304 if unchanged."
        }
      ],
      "get": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Offline-fill bundle for a single in-flight inspection",
        "description": "Returns the inspection + its template (with all template items) +\nattached media references + the regulatory KB excerpts the form\nrenderer needs to keep working after the network drops. Single\nround trip — the native client uses this on \"Open inspection\" so\nthe inspector can continue offline. Honors `If-None-Match` for\ncheap revalidation.\n",
        "responses": {
          "200": {
            "description": "Bundle payload",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "inspection",
                    "template",
                    "etag",
                    "fetched_at"
                  ],
                  "properties": {
                    "inspection": {
                      "$ref": "#/components/schemas/InspectionDetail"
                    },
                    "template": {
                      "$ref": "#/components/schemas/InspectionTemplateDetail"
                    },
                    "regulatory_kb": {
                      "description": "Reserved. Currently ALWAYS an empty array, for every tenant and every template — inspection templates carry no link to the regulatory knowledge base, so there is nothing for the server to resolve. The field is retained so the response shape stays stable for existing clients; it can only become populated once a template <-> knowledge base relationship exists. Do not build a client feature that depends on it being non-empty.",
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "title": {
                            "type": "string",
                            "nullable": true
                          },
                          "jurisdiction": {
                            "type": "string",
                            "nullable": true
                          },
                          "regulation": {
                            "type": "string",
                            "nullable": true
                          },
                          "summary": {
                            "type": "string",
                            "nullable": true
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "etag": {
                      "type": "string"
                    },
                    "fetched_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not modified — matches client's If-None-Match"
          },
          "404": {
            "description": "Inspection not found"
          }
        }
      }
    },
    "/inspections/inspections/{id}/review/approve": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Approve an inspection at the current review level",
        "description": "Advances the in_review inspection's approval workflow by one level.\nThe caller must satisfy `can_approve?` on the current\n`ApprovalLevel` (typically a manager or assigned reviewer).\nOnce the final level approves, the inspection transitions to\n`completed`.\n",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "comments": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Approved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "inspection": {
                      "$ref": "#/components/schemas/InspectionDetail"
                    },
                    "approval_request": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "status": {
                          "type": "string"
                        },
                        "current_level": {
                          "type": "integer",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Inspection is not in review / caller not eligible at this level"
          }
        }
      }
    },
    "/inspections/inspections/{id}/review/reject": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Reject an inspection at the current review level",
        "description": "Sends the inspection back to the inspector (transitions to\n`in_progress`) with the reviewer's required comment captured on\nthe `ApprovalRequest`. Comment is required — empty rejects 422.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "comments"
                ],
                "properties": {
                  "comments": {
                    "type": "string",
                    "minLength": 1
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rejected",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "inspection": {
                      "$ref": "#/components/schemas/InspectionDetail"
                    },
                    "approval_request": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "status": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Comment missing / inspection not in review / caller not eligible"
          }
        }
      }
    },
    "/inspections/inspections/{inspection_id}/items/{id}": {
      "parameters": [
        {
          "name": "inspection_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "patch": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Update a single inspection item",
        "description": "Sets compliance status / response value / notes / failure severity\non one inspection item. Only the assigned inspector (or a manager)\ncan update. `compliance_status` and `failure_severity` are\nallowlisted on the server — invalid values 422 with a clear error.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "compliance_status": {
                    "type": "string",
                    "enum": [
                      "pending",
                      "passed",
                      "failed",
                      "na",
                      "skipped"
                    ]
                  },
                  "response_value": {
                    "type": "string",
                    "nullable": true
                  },
                  "notes": {
                    "type": "string",
                    "nullable": true
                  },
                  "failure_severity": {
                    "type": "string",
                    "enum": [
                      "minor",
                      "moderate",
                      "severe",
                      "critical"
                    ],
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Item updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "item": {
                      "$ref": "#/components/schemas/InspectionItem"
                    },
                    "inspection_progress": {
                      "type": "number",
                      "format": "float",
                      "nullable": true
                    },
                    "score_breakdown": {
                      "type": "object",
                      "nullable": true,
                      "additionalProperties": true
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — caller cannot modify this inspection"
          },
          "404": {
            "description": "Inspection or item not found"
          },
          "422": {
            "description": "Invalid status / validation failed"
          }
        }
      }
    },
    "/inspections/inspections/{inspection_id}/items/{id}/media": {
      "parameters": [
        {
          "name": "inspection_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Attach a photo or video to an inspection item",
        "description": "Single canonical media-upload endpoint backed by `MediaItem`.\nTwo upload modes (same contract):\n  1. Multipart `{ file: <upload>, kind: 'photo'|'video' }` — the\n     API uploads through Rails in one request. Use when the client\n     has the bytes available locally.\n  2. Signed blob `{ blob_signed_id, kind }` — the client called\n     `POST /inspections/direct_uploads` to PUT bytes to S3 and is\n     now attaching the resulting blob.\n`kind` is optional when `content_type` starts with `image/` or\n`video/`; explicit otherwise.\n",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "photo",
                      "video"
                    ]
                  },
                  "blob_signed_id": {
                    "type": "string"
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "blob_signed_id": {
                    "type": "string"
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "photo",
                      "video"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Media attached",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "media_item": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "media_kind": {
                          "type": "string",
                          "enum": [
                            "photo",
                            "video"
                          ]
                        },
                        "content_type": {
                          "type": "string"
                        },
                        "byte_size": {
                          "type": "integer"
                        },
                        "original_filename": {
                          "type": "string",
                          "nullable": true
                        },
                        "url": {
                          "type": "string"
                        },
                        "thumb_url": {
                          "type": "string",
                          "nullable": true
                        },
                        "has_annotations": {
                          "type": "boolean"
                        },
                        "captured_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        }
                      }
                    },
                    "media_count": {
                      "type": "integer"
                    },
                    "item": {
                      "$ref": "#/components/schemas/InspectionItem"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Forbidden"
          },
          "422": {
            "description": "Invalid file / unsupported content type / file too large"
          }
        }
      }
    },
    "/inspections/inspections/{inspection_id}/items/{id}/media/{media_item_id}": {
      "parameters": [
        {
          "name": "inspection_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "media_item_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "delete": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Soft-delete a MediaItem attached to an inspection item",
        "description": "Soft-deletes the `MediaItem` (`deleted_at` set, file kept on S3).\nMirrors `MediaItemsController#destroy`: fires the subject hook so\na photo-required item that auto-passed via\n`on_media_item_attached` flips back to `pending` when the last\nphoto is removed.\n",
        "responses": {
          "200": {
            "description": "Media removed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "media_count": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Media item not found"
          }
        }
      }
    },
    "/inspections/inspections/{inspection_id}/items/{id}/annotation": {
      "parameters": [
        {
          "name": "inspection_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Save annotation data for a MediaItem",
        "description": "Persists a JSON annotation payload (drawing strokes, arrows,\nlabels) onto the named MediaItem. Matches the canonical\n`MediaItemsController` annotation contract — sets\n`annotation_data` + `has_annotations: true`.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "media_item_id",
                  "annotation_data"
                ],
                "properties": {
                  "media_item_id": {
                    "type": "integer"
                  },
                  "annotation_data": {
                    "type": "object",
                    "additionalProperties": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Annotation saved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "media_item_id": {
                      "type": "integer"
                    },
                    "annotation": {
                      "type": "object",
                      "additionalProperties": true
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Media item not found"
          },
          "422": {
            "description": "media_item_id or annotation_data missing"
          }
        }
      }
    },
    "/inspections/inspections/{inspection_id}/corrective_actions": {
      "parameters": [
        {
          "name": "inspection_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Create a corrective action nested under an inspection",
        "description": "Creates an `InspectionCorrectiveAction` linked to the parent\ninspection. `assigned_to_id` and `inspection_item_id` are\ncross-tenant validated — a user outside the business, or an item\nfrom another inspection, are rejected 422.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "corrective_action": {
                    "type": "object",
                    "required": [
                      "description"
                    ],
                    "properties": {
                      "inspection_item_id": {
                        "type": "integer",
                        "nullable": true
                      },
                      "description": {
                        "type": "string"
                      },
                      "priority": {
                        "type": "string",
                        "enum": [
                          "low",
                          "medium",
                          "high",
                          "critical"
                        ],
                        "default": "medium"
                      },
                      "due_date": {
                        "type": "string",
                        "format": "date",
                        "nullable": true
                      },
                      "assigned_to_id": {
                        "type": "integer",
                        "nullable": true
                      },
                      "status": {
                        "type": "string",
                        "enum": [
                          "pending",
                          "in_progress",
                          "completed",
                          "cancelled"
                        ],
                        "default": "pending"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "corrective_action": {
                      "$ref": "#/components/schemas/InspectionCorrectiveAction"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Inspection not found"
          },
          "422": {
            "description": "Invalid assignee / item / validation failed"
          }
        }
      }
    },
    "/inspections/templates/{id}/bundle": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "If-None-Match",
          "in": "header",
          "required": false,
          "schema": {
            "type": "string"
          },
          "description": "ETag from a prior bundle response; returns 304 if unchanged."
        }
      ],
      "get": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Offline pre-fetch bundle for an inspection template",
        "description": "Returns the full template + items + regulatory KB. The native\nclient uses this so an inspector can pick a template, drop the\nnetwork, fill an inspection locally, and `POST .../inspections`\nwith the buffered answers on reconnect.\n",
        "responses": {
          "200": {
            "description": "Template bundle",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "template",
                    "etag",
                    "fetched_at"
                  ],
                  "properties": {
                    "template": {
                      "$ref": "#/components/schemas/InspectionTemplateDetail"
                    },
                    "regulatory_kb": {
                      "description": "Reserved. Currently ALWAYS an empty array, for every tenant and every template — inspection templates carry no link to the regulatory knowledge base, so there is nothing for the server to resolve. The field is retained so the response shape stays stable for existing clients; it can only become populated once a template <-> knowledge base relationship exists. Do not build a client feature that depends on it being non-empty.",
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "title": {
                            "type": "string",
                            "nullable": true
                          },
                          "jurisdiction": {
                            "type": "string",
                            "nullable": true
                          },
                          "regulation": {
                            "type": "string",
                            "nullable": true
                          },
                          "summary": {
                            "type": "string",
                            "nullable": true
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "etag": {
                      "type": "string"
                    },
                    "fetched_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not modified — matches client's If-None-Match"
          },
          "404": {
            "description": "Template not found"
          }
        }
      }
    },
    "/inspections/schedules": {
      "get": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List inspection schedules",
        "description": "Read-only list of recurring inspection schedules. Schedule CRUD\nstays on the desktop admin surface — the native client only\nbrowses (\"what's due at my location\"). `?mine=true` restricts to\nschedules where the caller is the assigned inspector.\n",
        "parameters": [
          {
            "name": "active",
            "in": "query",
            "schema": {
              "type": "boolean"
            },
            "description": "Filter on active/inactive. Defaults to active when omitted."
          },
          {
            "name": "frequency",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "location_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "template_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "mine",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "Schedules",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/InspectionScheduleSummary"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PaginationMeta"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/inspections/schedules/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Fetch a single schedule",
        "responses": {
          "200": {
            "description": "Schedule detail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "schedule": {
                      "$ref": "#/components/schemas/InspectionScheduleSummary"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Schedule not found"
          }
        }
      }
    },
    "/inspections/schedules/due_today": {
      "get": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Schedules ready to generate now (\"due today\" feed)",
        "description": "Capped at 50 rows. Inspector-scoped by default; managers can\nrequest the team view with `?team=true`. Read-only — generation\nitself runs on the server via the scheduled job.\n",
        "parameters": [
          {
            "name": "team",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "Manager-only widen to team scope."
          }
        ],
        "responses": {
          "200": {
            "description": "Due-today schedules",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/InspectionScheduleSummary"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/inspections/claimable": {
      "get": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List items the caller can claim",
        "description": "Cross-cycle feed of every item the caller can claim right now — the\nmobile \"My Inspections → Available to claim\" surface. A claimable item is\na pending `InspectionCycleItem` (no inspection yet) on an active\n`per_item` cycle whose `assignment_strategy = claim_pool`, at a location\nthe caller is assigned to. Location-bounded for everyone (no manager\nbypass in the feed — a manager can still claim out-of-location items via\nthe claim endpoint, but won't see them here). Optional `cycle_id`\nnarrows to a single cycle.\n",
        "parameters": [
          {
            "name": "cycle_id",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Narrow the feed to a single cycle."
          },
          {
            "name": "location_id",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Narrow the feed to a single location. The feed is already bounded to the caller's assigned locations, so this only further filters within that set — an unassigned location returns no items.\n"
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "Claimable items",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/InspectionCycleItem"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PaginationMeta"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/inspections/cycles": {
      "get": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List inspection cycles",
        "description": "Read-only paginated list of cycles. Cycle creation / activation /\ncompletion stays on the desktop admin surface.\n",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "When omitted, defaults to active cycles only."
          },
          {
            "name": "template_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "Cycles",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/InspectionCycleSummary"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PaginationMeta"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/inspections/cycles/active": {
      "get": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Active-cycle banner + the caller's coverage sweeps",
        "description": "The \"My Inspections\" home surface, mirroring the desktop\n`apps/inspections/inspections#my_inspections` page as two buckets:\n\n- `active_cycle` — the single most-recently-activated active,\n  non-coverage_sweep cycle (the \"Start {template}\" banner). Not\n  audience-filtered — matches the web banner. Carries `my_inspection`\n  so the client shows Resume vs Start. Null when there is no active\n  startable cycle.\n- `sweeps` — coverage sweeps the caller can work, via the same\n  `InspectionSweep.for_inspector` rule the web uses (claim_pool → anyone\n  at the location; zones → assigned / session inspectors).\n",
        "responses": {
          "200": {
            "description": "Active-cycle banner + sweeps",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "active_cycle": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/InspectionCycleSummary"
                        }
                      ],
                      "nullable": true
                    },
                    "sweeps": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/InspectionSweepSummary"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/inspections/cycles/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Fetch a single cycle with progress",
        "responses": {
          "200": {
            "description": "Cycle detail with `progress` block",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "cycle": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/InspectionCycleSummary"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "progress": {
                              "type": "object",
                              "properties": {
                                "total": {
                                  "type": "integer"
                                },
                                "by_status": {
                                  "type": "object",
                                  "additionalProperties": {
                                    "type": "integer"
                                  }
                                }
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Cycle not found"
          }
        }
      }
    },
    "/inspections/cycles/{id}/start_inspection": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Start (or resume) an inspection for this cycle",
        "description": "Mobile equivalent of the desktop \"Start <template>\" cycle-banner button.\nResumes the caller's existing in-progress/draft inspection for this\ncycle if present (no duplicate); otherwise creates one linked to the\ncycle with the inspector's primary work location pre-attached. If the\ncycle's template requires training the caller hasn't completed, the\ninspection is created as a draft. Returns `201` when a new inspection\nis created and `200` when an existing one is resumed.\n",
        "responses": {
          "200": {
            "description": "Existing inspection resumed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "inspection": {
                      "$ref": "#/components/schemas/InspectionDetail"
                    },
                    "resumed": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "New inspection created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "inspection": {
                      "$ref": "#/components/schemas/InspectionDetail"
                    },
                    "resumed": {
                      "type": "boolean",
                      "example": false
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Cycle is not active or has no template configured",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Cycle not found"
          }
        }
      }
    },
    "/inspections/cycles/{cycle_id}/items/{id}/claim": {
      "parameters": [
        {
          "name": "cycle_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Claim one unassigned cycle item",
        "description": "Claim one item from the pool. Race-safe — a guarded UPDATE flips exactly\none of N simultaneous claimers; the winner gets a freshly-created\ninspection and can route straight into the editable form. Returns the\nsame `InspectionDetail` shape `start_inspection` returns. Eligibility\nmirrors the web: an inspections manager/admin (claims anywhere) or a\nmember assigned to the item's location. `409 already_claimed` is\nnon-retryable — show \"Already claimed\", refresh the list, do not open\nthe form.\n",
        "responses": {
          "201": {
            "description": "Item claimed — inspection created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "inspection": {
                      "$ref": "#/components/schemas/InspectionDetail"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`not_eligible` — caller is not assigned to the item's location",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`already_claimed` — another user won the race (non-retryable)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`cycle_inactive` — the cycle is no longer active",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Cycle or item not found"
          }
        }
      }
    },
    "/inspections/sweeps": {
      "get": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Sweeps the caller can work",
        "description": "The coverage sweeps this inspector can work (InspectionSweep.for_inspector) — same rule as the web \"Coverage sweeps\" section.",
        "responses": {
          "200": {
            "description": "Sweeps",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/InspectionSweepSummary"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/inspections/sweeps/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Full sweep detail (aisles + grid + findings)",
        "responses": {
          "200": {
            "description": "Sweep detail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sweep": {
                      "$ref": "#/components/schemas/InspectionSweepDetail"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not authorized for this sweep"
          },
          "404": {
            "description": "Sweep not found"
          }
        }
      }
    },
    "/inspections/sweeps/{id}/bundle": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Offline warm-up bundle",
        "description": "Full detail + log-issue form schema + components. Supports If-None-Match → 304.",
        "responses": {
          "200": {
            "description": "Bundle",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InspectionSweepBundle"
                }
              }
            }
          },
          "304": {
            "description": "Not modified (ETag matched)"
          }
        }
      }
    },
    "/inspections/sweeps/{id}/claim_aisle": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Claim the next unclaimed aisle (race-safe)",
        "description": "Idempotency-Key supported — a retried claim returns the same aisle instead of grabbing a second.",
        "responses": {
          "200": {
            "description": "Aisle claimed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "aisle": {
                      "$ref": "#/components/schemas/InspectionSweepAisle"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "`no_aisles` — nothing left to claim (the normal end-of-sweep outcome). `action_failed` — an unclassified server-side failure; treat it as retryable, NOT as \"the sweep is finished\"."
          }
        }
      }
    },
    "/inspections/sweeps/{id}/pause": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Pause / hand off",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "aisle_id": {
                    "type": "integer"
                  },
                  "handoff_note": {
                    "type": "string"
                  },
                  "frontier_level": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Session ended"
          }
        }
      }
    },
    "/inspections/sweeps/{id}/sync": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Offline batch flush",
        "description": "Buffered cells + findings + resets + resume cursor in one request. Idempotency-Key dedupes a replayed batch; a signed-off sweep rejects writes (409).",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "cells": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "aisle_id": {
                          "type": "integer"
                        },
                        "bay": {
                          "type": "integer"
                        },
                        "level": {
                          "type": "integer"
                        }
                      }
                    }
                  },
                  "exceptions": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "Each item mirrors the POST /exceptions body (component, severity, bay, level, answers, and photos via photo_ids or blob_signed_ids)."
                  },
                  "resets": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "aisle_id": {
                          "type": "integer"
                        },
                        "bay": {
                          "type": "integer"
                        }
                      }
                    }
                  },
                  "frontier": {
                    "type": "object",
                    "properties": {
                      "aisle_id": {
                        "type": "integer"
                      },
                      "bay": {
                        "type": "integer"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Applied",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sweep": {
                      "$ref": "#/components/schemas/InspectionSweepDetail"
                    },
                    "applied": {
                      "type": "object",
                      "properties": {
                        "cells": {
                          "type": "integer"
                        },
                        "exceptions": {
                          "type": "integer"
                        },
                        "resets": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "`sweep_locked` — sweep is signed off"
          }
        }
      }
    },
    "/inspections/sweeps/{id}/sign_off": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Complete / sign off the sweep",
        "description": "Routes for approval when the template requires it, else signs off. 422 while a critical finding is open or already pending review.",
        "responses": {
          "200": {
            "description": "Signed off or sent for approval",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "description": "signed_off | pending_approval"
                    },
                    "sweep": {
                      "$ref": "#/components/schemas/InspectionSweepSummary"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "`unresolved_criticals` or `already_pending`"
          }
        }
      }
    },
    "/inspections/sweeps/{id}/review/approve": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Approve a pending review",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "comments": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Approved (may sign off on final level)"
          },
          "422": {
            "description": "`cannot_review` — not awaiting review / not eligible"
          }
        }
      }
    },
    "/inspections/sweeps/{id}/review/reject": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Reject a pending review (returns for rework)",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "comments"
                ],
                "properties": {
                  "comments": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Returned for rework"
          },
          "422": {
            "description": "`cannot_review` — missing reason / not eligible"
          }
        }
      }
    },
    "/inspections/sweeps/{sweep_id}/aisles/{id}/mark_passed": {
      "parameters": [
        {
          "name": "sweep_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Mark a shelf (bay, level) passed",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "bay",
                  "level"
                ],
                "properties": {
                  "bay": {
                    "type": "integer"
                  },
                  "level": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Aisle (with updated grid)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "aisle": {
                      "$ref": "#/components/schemas/InspectionSweepAisle"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "`out_of_range`"
          }
        }
      }
    },
    "/inspections/sweeps/{sweep_id}/aisles/{id}/reset_bay": {
      "parameters": [
        {
          "name": "sweep_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Reset one bay to pending (keeps logged findings)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "bay"
                ],
                "properties": {
                  "bay": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Aisle",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "aisle": {
                      "$ref": "#/components/schemas/InspectionSweepAisle"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/inspections/sweeps/{sweep_id}/aisles/{id}/reset_cell": {
      "parameters": [
        {
          "name": "sweep_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Un-mark one shelf (toggle a passed cell back to pending)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "bay",
                  "level"
                ],
                "properties": {
                  "bay": {
                    "type": "integer"
                  },
                  "level": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Aisle",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "aisle": {
                      "$ref": "#/components/schemas/InspectionSweepAisle"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/inspections/sweeps/{sweep_id}/aisles/{id}/reset_aisle": {
      "parameters": [
        {
          "name": "sweep_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Reset the whole aisle (clears cells + soft-voids findings)",
        "responses": {
          "200": {
            "description": "Aisle",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "aisle": {
                      "$ref": "#/components/schemas/InspectionSweepAisle"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/inspections/sweeps/{sweep_id}/aisles/{id}/cover": {
      "parameters": [
        {
          "name": "sweep_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Bulk-cover the aisle (pass all non-exception shelves)",
        "responses": {
          "200": {
            "description": "Aisle covered",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "aisle": {
                      "$ref": "#/components/schemas/InspectionSweepAisle"
                    },
                    "next_aisle_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "done": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/inspections/sweeps/{sweep_id}/aisles/{id}/skip": {
      "parameters": [
        {
          "name": "sweep_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "patch": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Skip an aisle (obstruction / unreachable)",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "skip_reason": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Aisle",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "aisle": {
                      "$ref": "#/components/schemas/InspectionSweepAisle"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/inspections/sweeps/{sweep_id}/aisles/{id}/unskip": {
      "parameters": [
        {
          "name": "sweep_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "patch": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Restore a skipped aisle",
        "responses": {
          "200": {
            "description": "Aisle",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "aisle": {
                      "$ref": "#/components/schemas/InspectionSweepAisle"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/inspections/sweeps/{sweep_id}/aisles/{id}/advance_frontier": {
      "parameters": [
        {
          "name": "sweep_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "patch": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Manager manually sets the frontier",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "frontier_bay"
                ],
                "properties": {
                  "frontier_bay": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Aisle",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "aisle": {
                      "$ref": "#/components/schemas/InspectionSweepAisle"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/inspections/sweeps/{sweep_id}/exceptions": {
      "parameters": [
        {
          "name": "sweep_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List findings",
        "parameters": [
          {
            "name": "severity",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "aisle_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Findings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/InspectionSweepException"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Log a finding",
        "description": "Attach photos by EITHER blob_signed_ids (recommended: direct-upload bytes via /inspections/direct_uploads, then send the signed ids) OR photo_ids (pending inbox MediaItem ids). Idempotency-Key dedupes a replayed POST.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "aisle_id": {
                    "type": "integer"
                  },
                  "bay": {
                    "type": "string"
                  },
                  "level": {
                    "type": "string"
                  },
                  "component": {
                    "type": "string"
                  },
                  "damage": {
                    "type": "string"
                  },
                  "severity": {
                    "type": "string",
                    "description": "case-insensitive: observation|minor|major|critical"
                  },
                  "note": {
                    "type": "string"
                  },
                  "answers": {
                    "type": "object"
                  },
                  "photo_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "Pending inbox MediaItem ids"
                  },
                  "blob_signed_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Signed Active Storage blob ids from /inspections/direct_uploads"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Finding logged",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "exception": {
                      "$ref": "#/components/schemas/InspectionSweepException"
                    },
                    "corrective_action_created": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/inspections/sweeps/{sweep_id}/exceptions/{id}": {
      "parameters": [
        {
          "name": "sweep_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Finding detail",
        "responses": {
          "200": {
            "description": "Finding",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "exception": {
                      "$ref": "#/components/schemas/InspectionSweepException"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found"
          }
        }
      },
      "patch": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Edit a logged finding",
        "description": "Only provided fields change (omitted = unchanged; empty string clears). Status is preserved — use resolve/escalate to change it. Photos (photo_ids / blob_signed_ids) are additive. Moving bay/level/aisle repositions the red grid cell.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "aisle_id": {
                    "type": "integer"
                  },
                  "bay": {
                    "type": "string"
                  },
                  "level": {
                    "type": "string"
                  },
                  "component": {
                    "type": "string"
                  },
                  "damage": {
                    "type": "string"
                  },
                  "severity": {
                    "type": "string",
                    "description": "case-insensitive: observation|minor|major|critical"
                  },
                  "note": {
                    "type": "string"
                  },
                  "answers": {
                    "type": "object"
                  },
                  "photo_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "Pending inbox MediaItem ids (additive)"
                  },
                  "blob_signed_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Signed Active Storage blob ids (additive)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated finding",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "exception": {
                      "$ref": "#/components/schemas/InspectionSweepException"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Update failed"
          },
          "404": {
            "description": "Not found"
          }
        }
      },
      "delete": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Delete (soft-void) a logged finding",
        "description": "Soft-voids the finding — clears its red grid cell and hides it from the finding lists (index, sweep detail, notable_exceptions), while preserving the audited Inspection row and any corrective action. Not reversible via the API.",
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "404": {
            "description": "Not found"
          },
          "422": {
            "description": "Delete failed"
          }
        }
      }
    },
    "/inspections/sweeps/{sweep_id}/exceptions/{id}/resolve": {
      "parameters": [
        {
          "name": "sweep_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Mark a finding resolved",
        "responses": {
          "200": {
            "description": "Finding",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "exception": {
                      "$ref": "#/components/schemas/InspectionSweepException"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/inspections/sweeps/{sweep_id}/exceptions/{id}/escalate": {
      "parameters": [
        {
          "name": "sweep_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Escalate a finding (opens a corrective action)",
        "responses": {
          "200": {
            "description": "Finding",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "exception": {
                      "$ref": "#/components/schemas/InspectionSweepException"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/inspections/bundles/sync_manifest": {
      "get": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "App-start warm-up bundle",
        "description": "One round-trip payload the native client fetches on app start\n(and after returning from offline). Pre-caches templates, the\ncaller's open inspections, the caller's open corrective actions,\nand upcoming schedules — all sized so a single HTTP call covers\na cold launch. Honors `If-None-Match` for cheap revalidation.\n",
        "parameters": [
          {
            "name": "include",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated subset of\n`templates,inspections,corrective_actions,schedules`. Defaults\nto all four.\n"
          },
          {
            "name": "days_ahead",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 7,
              "minimum": 1,
              "maximum": 60
            },
            "description": "Schedules window in days from now."
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Manifest payload",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "fetched_at",
                    "etag"
                  ],
                  "properties": {
                    "templates": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/InspectionTemplateSummary"
                      }
                    },
                    "inspections": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/InspectionSummary"
                      }
                    },
                    "corrective_actions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/InspectionCorrectiveAction"
                      }
                    },
                    "schedules": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/InspectionScheduleSummary"
                      }
                    },
                    "fetched_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "etag": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not modified — matches client's If-None-Match"
          }
        }
      }
    },
    "/messaging/threads": {
      "get": {
        "tags": [
          "Messaging"
        ],
        "summary": "List conversations (chat list)",
        "description": "Paginated list of the caller's direct-message conversations, newest\nactivity first. Each row carries an unread count and a compact\nlast-message preview (with an attachments flag).\n\nBy default this returns the caller's **live** conversations. Pass\n`archived=true` to return the caller's **archived** conversations\ninstead (the ones hidden from the live list once the caller archives\nthem; a new message resurfaces a thread back to the live list). Archive\nis per-caller, so the two slices differ per user on shared group threads.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number (1-indexed)",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "archived",
            "in": "query",
            "required": false,
            "description": "When true, returns the caller's archived conversations instead of the live list. Accepts true/false/1/0; defaults to false (live list).",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated conversation list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MessagingThread"
                      }
                    },
                    "total_count": {
                      "type": "integer"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/MessagingPaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Messages app disabled or insufficient scope"
          }
        }
      },
      "post": {
        "tags": [
          "Messaging"
        ],
        "summary": "Start a conversation (and optionally post the first message)",
        "description": "Creates a 1:1 conversation (one recipient — idempotent, returns the\nexisting thread if one exists) or a group conversation (2+ recipients).\nAn optional `body` (+ `attachments[]`) posts the first message inline.\nGovernance (`who_can_initiate`, group-DMs-enabled, participant cap) is\nenforced server-side.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "user_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "Recipient user ids (excluding the caller)."
                  },
                  "title": {
                    "type": "string",
                    "description": "Optional group title."
                  },
                  "body": {
                    "type": "string",
                    "description": "Optional first message."
                  },
                  "attachments": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "Optional first-message file attachments."
                  },
                  "context_workspace_id": {
                    "type": "integer",
                    "description": "Optional origin workspace id."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Conversation created (or existing 1:1 returned)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "thread": {
                      "$ref": "#/components/schemas/MessagingThreadDetail"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Not allowed to start conversations / group DMs disabled"
          },
          "422": {
            "description": "No recipients / participant cap exceeded"
          }
        }
      }
    },
    "/messaging/threads/unread_count": {
      "get": {
        "tags": [
          "Messaging"
        ],
        "summary": "Total unread DM badge",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Unread total",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "unread": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    },
    "/messaging/recipients": {
      "get": {
        "tags": [
          "Messaging"
        ],
        "summary": "Recipient picker (searchable org users)",
        "description": "Searchable, recent-DM-boosted list of users the caller can message —\nthe same ranking the web \"New Message\" picker uses. Scoped to the\ncaller's business.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Search term (name / email / employee id / phone / title).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ranked user results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MessagingRecipient"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "more": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    },
    "/messaging/threads/{id}": {
      "get": {
        "tags": [
          "Messaging"
        ],
        "summary": "Open a conversation (marks it read)",
        "description": "Returns the conversation with its participants and marks it read for the caller.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Conversation detail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "thread": {
                      "$ref": "#/components/schemas/MessagingThreadDetail"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Conversation not found (or caller not a participant)"
          }
        }
      }
    },
    "/messaging/threads/{id}/read": {
      "patch": {
        "tags": [
          "Messaging"
        ],
        "summary": "Mark a conversation read",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Marked read"
          },
          "404": {
            "description": "Conversation not found"
          }
        }
      }
    },
    "/messaging/threads/{id}/archive": {
      "post": {
        "tags": [
          "Messaging"
        ],
        "summary": "Archive a conversation (per-user)",
        "description": "Archives the conversation for the CALLER only — stamps `archived_at` on\nthe caller's own participant row so it drops out of their live list\n(`GET /messaging/threads`) and appears under `?archived=true`. Per-user\nand never touches another participant's row or the thread itself. An\narchived conversation resurfaces automatically the next time anyone posts\nto it; use `POST /messaging/threads/{id}/unarchive` to bring it back\nmanually. Same semantics as the web Messages \"Archive\" action.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Archived",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Messages app disabled or insufficient scope (needs write:messages)"
          },
          "404": {
            "description": "Conversation not found (or caller not a participant)"
          }
        }
      }
    },
    "/messaging/threads/{id}/unarchive": {
      "post": {
        "tags": [
          "Messaging"
        ],
        "summary": "Unarchive a conversation (per-user)",
        "description": "Clears the caller's archive (`archived_at` → null) so the conversation\nreturns to their live list (`GET /messaging/threads`). The explicit\ninverse of `POST /messaging/threads/{id}/archive` — archived conversations\nalso resurface implicitly when anyone posts a new message, but this lets a\ncaller pull one back without waiting. Per-user (never touches another\nparticipant's row) and idempotent — unarchiving a conversation that isn't\narchived still returns 200.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Unarchived",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Messages app disabled or insufficient scope (needs write:messages)"
          },
          "404": {
            "description": "Conversation not found (or caller not a participant)"
          }
        }
      }
    },
    "/messaging/threads/{id}/rename": {
      "patch": {
        "tags": [
          "Messaging"
        ],
        "summary": "Rename a group conversation",
        "description": "Sets a custom title on a GROUP conversation (1:1 conversations cannot be\nrenamed → 422). The title is trimmed and capped at 120 characters; a\nblank title clears the custom name and the conversation falls back to its\nparticipant-derived label. Same semantics as the web Messages\n\"Rename conversation\" action. Returns the detailed thread payload.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 120,
                    "description": "New conversation name. Blank clears the custom name.",
                    "example": "Launch War Room"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Renamed — detailed thread payload"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Messages app disabled or insufficient scope (needs write:messages)"
          },
          "404": {
            "description": "Conversation not found (or caller not a participant)"
          },
          "422": {
            "description": "Not renameable (1:1 conversation) or title too long",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "thread_not_renamed"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/messaging/threads/{id}/leave": {
      "post": {
        "tags": [
          "Messaging"
        ],
        "summary": "Leave a group conversation",
        "description": "Removes the CALLER's own participant row from a GROUP conversation. The\nthread itself is untouched — remaining participants keep the full history\nand the leaver simply stops seeing (and being notified about) the\nconversation. 1:1 conversations cannot be left → 422; use archive to hide\na 1:1 instead. Same semantics as the web Messages \"Leave\nconversation\" action. Removes only the caller — there is no endpoint to\nremove another member.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Left the conversation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Messages app disabled or insufficient scope (needs write:messages)"
          },
          "404": {
            "description": "Conversation not found (or caller not a participant)"
          },
          "422": {
            "description": "Not leaveable (1:1 conversation)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "thread_not_left"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/messaging/threads/{id}/mute": {
      "post": {
        "tags": [
          "Messaging"
        ],
        "summary": "Mute a conversation (per-user)",
        "description": "Mutes the conversation for the CALLER only — writes `muted_until` on the\ncaller's own participant row (≈ 100 years out). While muted the message\nnotifier skips the caller and the conversation drops out of the unread\nbadge total; per-row unread counts are unaffected. Same semantics as the\nweb Messages 3-dot \"Mute conversation\" action. Idempotent.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Muted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "muted": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Messages app disabled or insufficient scope (needs write:messages)"
          },
          "404": {
            "description": "Conversation not found (or caller not a participant)"
          }
        }
      }
    },
    "/messaging/threads/{id}/unmute": {
      "post": {
        "tags": [
          "Messaging"
        ],
        "summary": "Unmute a conversation (per-user)",
        "description": "Clears the caller's mute (`muted_until` → null) so notifications and\nbadge counts resume. Same semantics as the web \"Unmute\" action.\nIdempotent — unmuting an already-unmuted conversation still returns 200.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Unmuted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "muted": {
                      "type": "boolean",
                      "example": false
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Messages app disabled or insufficient scope (needs write:messages)"
          },
          "404": {
            "description": "Conversation not found (or caller not a participant)"
          }
        }
      }
    },
    "/messaging/threads/{id}/add_participants": {
      "post": {
        "tags": [
          "Messaging"
        ],
        "summary": "Add members to a group conversation",
        "description": "Adds one or more people to a GROUP conversation (1:1 conversations\ncannot take new members → 422). Resolution is STRICT: if any requested\n`user_id` doesn't resolve inside the caller's business the whole call\nfails and nobody is added — never a partial add. Users already in the\nconversation are skipped gracefully (re-adding is a no-op, still 200).\nThe group participant cap is enforced across existing + new members.\nSame semantics as the web Messages \"Add people\" action. Returns the\nrefreshed participant roster.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "user_ids"
                ],
                "properties": {
                  "user_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "IDs of the users to add. Must all resolve inside the caller's business.",
                    "example": [
                      42,
                      57
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Members added — refreshed participant roster",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "participants": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MessagingParticipant"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Messages app disabled or insufficient scope (needs write:messages)"
          },
          "404": {
            "description": "Conversation not found (or caller not a participant)"
          },
          "422": {
            "description": "Not a group conversation, no/invalid user_ids, or participant cap exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "participants_not_added"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/messaging/threads/{id}/participants": {
      "get": {
        "tags": [
          "Messaging"
        ],
        "summary": "List the conversation's users",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Participants",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "participants": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MessagingParticipant"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Conversation not found"
          }
        }
      }
    },
    "/messaging/threads/{thread_id}/messages": {
      "get": {
        "tags": [
          "Messaging"
        ],
        "summary": "List messages in a conversation",
        "description": "Message history, newest first.\n\nTwo modes. Without `before_id` this is page/offset based and the\nresponse carries `meta`. Offset paging is not stable over a live\nconversation — a message posted between two fetches shifts the window\nand the next page repeats rows, one deleted between fetches skips a row\n— so pass `before_id` to walk the history with a cursor instead: the\n`per_page` messages strictly older than that message id. The cursor\nresponse carries `cursor` instead of `meta`; feed `cursor.next_before_id`\nback as `before_id` and stop when `cursor.has_more` is false.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "thread_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Ignored when `before_id` is given.",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 30,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "before_id",
            "in": "query",
            "description": "Message id cursor. Returns the `per_page` messages older than this id, newest first. Optional; omit for page-based paging.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated messages",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MessagingMessage"
                      }
                    },
                    "total_count": {
                      "type": "integer"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/MessagingPaginationMeta"
                    },
                    "cursor": {
                      "type": "object",
                      "description": "Present only when `before_id` was given (replaces `meta`).",
                      "properties": {
                        "before_id": {
                          "type": "integer"
                        },
                        "next_before_id": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Oldest id in this window; pass back as `before_id`. Null when the window is empty."
                        },
                        "has_more": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Conversation not found"
          }
        }
      },
      "post": {
        "tags": [
          "Messaging"
        ],
        "summary": "Post a message (text + attachments)",
        "description": "multipart/form-data — `body` text and/or repeated `attachments[]` file\nparts. Attachments are dropped if file uploads are disabled for the\nbusiness; an attachment-only post then fails the empty-message guard.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "thread_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "body": {
                    "type": "string"
                  },
                  "attachments": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Message posted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "$ref": "#/components/schemas/MessagingMessage"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Conversation not found"
          },
          "422": {
            "description": "Empty message / not a participant"
          }
        }
      }
    },
    "/messaging/threads/{thread_id}/messages/{id}": {
      "patch": {
        "tags": [
          "Messaging"
        ],
        "summary": "Edit a message (author only)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "thread_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "properties": {
                  "body": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Message updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "$ref": "#/components/schemas/MessagingMessage"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Message not found or not the caller's"
          },
          "422": {
            "description": "Blank body"
          }
        }
      },
      "delete": {
        "tags": [
          "Messaging"
        ],
        "summary": "Delete a message (author only, soft delete)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "thread_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Message soft-deleted (returns the tombstoned message)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "$ref": "#/components/schemas/MessagingMessage"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Message not found or not the caller's"
          }
        }
      }
    },
    "/messaging/threads/{thread_id}/messages/{message_id}/attachments/{id}": {
      "delete": {
        "tags": [
          "Messaging"
        ],
        "summary": "Remove one attachment (author only)",
        "description": "DMs are private — admins/managers do NOT bypass the author check.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "thread_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "message_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Attachment removed"
          },
          "403": {
            "description": "Not the caller's message"
          },
          "404": {
            "description": "Attachment or conversation not found"
          }
        }
      }
    },
    "/chat/rooms": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "List the caller's chat rooms",
        "description": "Paginated list of the caller's active rooms (direct / group / channel),\nwith unread counts, pin/mute state, a compact last-message preview, and\nper-room capability flags.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "scope",
            "in": "query",
            "description": "Filter by room type. Omit for all rooms.",
            "schema": {
              "type": "string",
              "enum": [
                "direct",
                "group",
                "channel"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated room list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "rooms": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ChatRoom"
                      }
                    },
                    "meta": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ChatPaginationMeta"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "scope": {
                              "type": "string",
                              "nullable": true,
                              "enum": [
                                "direct",
                                "group",
                                "channel"
                              ],
                              "description": "The room-type filter ACTUALLY applied, normalized\n(case and surrounding whitespace are forgiven), or\nnull when none was. An unrecognized `scope` fails\nopen to the full list rather than erroring - read\nthis key rather than assuming the request's own\nvalue was honoured.\n"
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled or insufficient scope"
          }
        }
      },
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Create a room (DM or group/channel)",
        "description": "Creates a direct message (`room_type: direct` + `other_user_id`) or a\ngroup/channel (`room_type: group|channel` + `name` + `member_user_ids`).\nDMs are idempotent — an existing DM with that user is returned with\n200 instead of 201. Gated by the `direct_messages_enabled` /\n`group_chats_enabled` business settings.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "room_type"
                ],
                "properties": {
                  "room_type": {
                    "type": "string",
                    "enum": [
                      "direct",
                      "group",
                      "channel"
                    ]
                  },
                  "other_user_id": {
                    "type": "integer",
                    "description": "DM peer (direct rooms only)."
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 255,
                    "description": "Group/channel name (required for non-DM rooms)."
                  },
                  "member_user_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "Initial members (group/channel; excludes the caller)."
                  },
                  "photo": {
                    "type": "string",
                    "format": "binary",
                    "description": "Optional group/channel photo."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Existing direct room returned (idempotent DM create)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room": {
                      "$ref": "#/components/schemas/ChatRoom"
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Room created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room": {
                      "$ref": "#/components/schemas/ChatRoom"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing member_user_ids / blank or over-long name"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled or DMs/group chats disabled"
          },
          "422": {
            "description": "Peer is self / users not in this business"
          }
        }
      }
    },
    "/chat/rooms/{id}": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "Room detail + message timeline",
        "description": "Returns the room plus a message page ordered newest-first, paginated by\nmessage id (`before_id` cursor). The initial page (`before_id=0`) also\nmarks the room read for the caller.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "before_id",
            "in": "query",
            "description": "Return messages strictly older than this id (0 = newest page).",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 100
            }
          },
          {
            "name": "include_thread_replies",
            "in": "query",
            "description": "Include thread replies inline in the timeline.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Room + message page",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room": {
                      "$ref": "#/components/schemas/ChatRoom"
                    },
                    "messages": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ChatMessage"
                      }
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "oldest_message_id": {
                      "type": "integer",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled or not a room member"
          },
          "404": {
            "description": "Room not found"
          }
        }
      },
      "patch": {
        "tags": [
          "Chat"
        ],
        "summary": "Update a group/channel (room admin only)",
        "description": "Rename and/or change the photo of a group/channel. Direct rooms cannot\nbe edited. At least one of `name`, `photo`, `remove_photo` is required.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 255
                  },
                  "photo": {
                    "type": "string",
                    "format": "binary",
                    "description": "PNG/JPEG/GIF/WebP, max 10 MB."
                  },
                  "remove_photo": {
                    "type": "boolean",
                    "description": "Clear the existing photo."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Room updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room": {
                      "$ref": "#/components/schemas/ChatRoom"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a member or not a room admin"
          },
          "404": {
            "description": "Room not found"
          },
          "422": {
            "description": "Direct room / no updates given / invalid name or photo"
          }
        }
      },
      "delete": {
        "tags": [
          "Chat"
        ],
        "summary": "Leave a room (or hard-delete with for_all=true)",
        "description": "Default is a per-user soft leave (no-op for DMs). `for_all=true`\nhard-deletes the room for everyone — room creator or chat app admin\nonly.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "for_all",
            "in": "query",
            "description": "Hard-delete the room for all members (creator/admin only).",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Left or deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room_id": {
                      "type": "integer"
                    },
                    "archived": {
                      "type": "boolean"
                    },
                    "left": {
                      "type": "boolean"
                    },
                    "deleted": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a member",
            "or not creator/admin for for_all": null
          },
          "404": {
            "description": "Room not found"
          },
          "422": {
            "description": "Direct rooms cannot be hard-deleted"
          }
        }
      }
    },
    "/chat/rooms/mark_all_read": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Mark every conversation read (conversation-list \"Mark all as read\")",
        "description": "Bulk twin of `PATCH /chat/rooms/{id}/mark_read`, backing the conversation list's \"⋯ → Mark all as read\". Sweeps every room the caller's list shows — not one page and not one filter — advancing each read cursor and clearing each unread count in a single statement. Idempotent: a second call with nothing unread returns `marked_count: 0`.\n\n`marked_count` / `room_ids` report the conversations that were actually SHOWING as unread, so a client can quote the number and clear exactly those badges. Rooms whose cursor merely caught up (their newest message is the caller's own) are swept too but are not counted.\n\n`awaiting_ack_room_ids` names the rooms that still carry a pending Important or Read-Receipt acknowledgement. Marking read deliberately does NOT acknowledge on the user's behalf, so those rooms keep their `has_important_messages` / `has_read_receipt_messages` flag on the next list fetch — clients should explain that rather than treat it as the action failing.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Sweep applied",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "marked_count": {
                      "type": "integer",
                      "description": "Conversations that were unread and now are not"
                    },
                    "room_ids": {
                      "type": "array",
                      "items": {
                        "type": "integer"
                      },
                      "description": "Ids of those conversations"
                    },
                    "awaiting_ack_room_ids": {
                      "type": "array",
                      "items": {
                        "type": "integer"
                      },
                      "description": "Rooms still holding an unacknowledged Important / Read-Receipt message"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled",
            "or token lacks write:chat": null
          }
        }
      }
    },
    "/chat/rooms/{id}/mark_read": {
      "patch": {
        "tags": [
          "Chat"
        ],
        "summary": "Mark all messages in a room read (idempotent)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Read cursor advanced (noop=true when nothing was unread)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room_id": {
                      "type": "integer"
                    },
                    "noop": {
                      "type": "boolean"
                    },
                    "last_read_message_id": {
                      "type": "integer",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled or not a room member"
          },
          "404": {
            "description": "Room not found"
          }
        }
      }
    },
    "/chat/rooms/{id}/mentionable_users": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "Mention typeahead within a room",
        "description": "Search the room's members by name/email for @-mention typeahead.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Search query (min 2 chars to filter).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching members",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "users": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ChatUserSummary"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled"
          },
          "404": {
            "description": "Room not found"
          }
        }
      }
    },
    "/chat/rooms/{id}/files": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "Paginated attachment list for a room",
        "description": "All file/media attachments shared in the room, newest first.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 30,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated file list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "files": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "message_id": {
                            "type": "integer"
                          },
                          "filename": {
                            "type": "string"
                          },
                          "content_type": {
                            "type": "string"
                          },
                          "byte_size": {
                            "type": "integer"
                          },
                          "url": {
                            "type": "string"
                          },
                          "preview_url": {
                            "type": "string",
                            "nullable": true
                          },
                          "uploaded_by": {
                            "nullable": true,
                            "allOf": [
                              {
                                "$ref": "#/components/schemas/ChatUserSummary"
                              }
                            ]
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "meta": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ChatPaginationMeta"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "has_more": {
                              "type": "boolean"
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled or not a room member"
          },
          "404": {
            "description": "Room not found"
          }
        }
      }
    },
    "/chat/rooms/{id}/pin": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Pin a room for the caller (idempotent)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Room pinned",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room_id": {
                      "type": "integer"
                    },
                    "pinned_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled or not a room member"
          },
          "404": {
            "description": "Room not found"
          }
        }
      },
      "delete": {
        "tags": [
          "Chat"
        ],
        "summary": "Unpin a room for the caller (idempotent)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Room unpinned",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room_id": {
                      "type": "integer"
                    },
                    "pinned_at": {
                      "type": "string",
                      "nullable": true,
                      "description": "Always null after unpin."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled or not a room member"
          },
          "404": {
            "description": "Room not found"
          }
        }
      }
    },
    "/chat/rooms/{id}/mute": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Mute a room for the caller",
        "description": "Mute until a specific time (`muted_until` in epoch milliseconds) or\nindefinitely (0 / null / omitted).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "muted_until": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Epoch milliseconds; 0/null = mute indefinitely."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Room muted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room_id": {
                      "type": "integer"
                    },
                    "muted_until": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled or not a room member"
          },
          "404": {
            "description": "Room not found"
          }
        }
      },
      "delete": {
        "tags": [
          "Chat"
        ],
        "summary": "Unmute a room for the caller (idempotent)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Room unmuted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room_id": {
                      "type": "integer"
                    },
                    "muted_until": {
                      "type": "string",
                      "nullable": true,
                      "description": "Always null after unmute."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled or not a room member"
          },
          "404": {
            "description": "Room not found"
          }
        }
      }
    },
    "/chat/rooms/{id}/important_messages": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "Unread important messages in a room",
        "description": "Important messages (`ack_type=important`) the caller has not yet\nacknowledged — powers the client's \"unread important\" modal.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Unacknowledged important messages",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "messages": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ChatMessage"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled or not a room member"
          },
          "404": {
            "description": "Room not found"
          }
        }
      }
    },
    "/chat/rooms/{room_id}/members": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "List room members",
        "description": "Active members, admins first, then by join date, then by id. Paginated:\nthe default page size (200) is larger than any roster we have seen, so\nan existing caller that ignores `page` is not truncated today — but a\nroster above the page size IS truncated, and `meta` is the only thing\nthat says so. This block was undocumented until 2026-09-02 while the\nendpoint was already paginating, which is why it is spelled out here.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 200,
              "minimum": 1,
              "maximum": 500
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated member list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "members": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ChatMember"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ChatPaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled or not a room member"
          },
          "404": {
            "description": "Room not found"
          }
        }
      },
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Add members to a group/channel (room admin only)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "user_ids"
                ],
                "properties": {
                  "user_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "Users to add (member_user_ids also accepted)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Members added (partitioned by outcome)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room_id": {
                      "type": "integer"
                    },
                    "added": {
                      "type": "array",
                      "items": {
                        "type": "integer"
                      }
                    },
                    "rejoined": {
                      "type": "array",
                      "items": {
                        "type": "integer"
                      }
                    },
                    "already_member": {
                      "type": "array",
                      "items": {
                        "type": "integer"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Not a member or not a room admin"
          },
          "404": {
            "description": "Room not found"
          },
          "422": {
            "description": "Direct room / empty user_ids / users not in business"
          }
        }
      }
    },
    "/chat/rooms/{room_id}/members/{id}": {
      "patch": {
        "tags": [
          "Chat"
        ],
        "summary": "Change a member's role (room admin only)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "Membership id (not user id)."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "role"
                ],
                "properties": {
                  "role": {
                    "type": "string",
                    "enum": [
                      "admin",
                      "member"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Role updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "member": {
                      "$ref": "#/components/schemas/ChatMember"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a member or not a room admin"
          },
          "404": {
            "description": "Room or member not found"
          },
          "422": {
            "description": "Invalid role / cannot demote the last admin"
          }
        }
      },
      "delete": {
        "tags": [
          "Chat"
        ],
        "summary": "Remove a member (self, or any member as room admin)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "Membership id (not user id)."
          }
        ],
        "responses": {
          "200": {
            "description": "Member removed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room_id": {
                      "type": "integer"
                    },
                    "membership_id": {
                      "type": "integer"
                    },
                    "removed_user_id": {
                      "type": "integer"
                    },
                    "self_left": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not authorized to remove this member"
          },
          "404": {
            "description": "Room or member not found"
          },
          "422": {
            "description": "Direct room / creator leaving / last admin"
          }
        }
      }
    },
    "/chat/rooms/{room_id}/media": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Start a direct-to-S3 media upload",
        "description": "Creates an unattached upload row and returns a presigned S3 PUT URL\n(1-hour TTL). The client PUTs the bytes to `upload_url`, then calls\nthe `/complete` endpoint, and finally references the media id in\n`media_ids` when posting a message.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "media_type",
                  "mime_type",
                  "original_filename",
                  "file_size_bytes"
                ],
                "properties": {
                  "media_type": {
                    "type": "string",
                    "enum": [
                      "image",
                      "gif",
                      "video",
                      "file"
                    ]
                  },
                  "mime_type": {
                    "type": "string",
                    "example": "image/jpeg"
                  },
                  "original_filename": {
                    "type": "string"
                  },
                  "file_size_bytes": {
                    "type": "integer"
                  },
                  "width_px": {
                    "type": "integer",
                    "description": "Images/video only."
                  },
                  "height_px": {
                    "type": "integer",
                    "description": "Images/video only."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Upload initiated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "media": {
                      "$ref": "#/components/schemas/ChatMedia"
                    },
                    "upload_url": {
                      "type": "string",
                      "description": "Presigned S3 PUT URL."
                    },
                    "storage_key": {
                      "type": "string"
                    },
                    "expires_in": {
                      "type": "integer",
                      "description": "Seconds until upload_url expires."
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a room member or file uploads disabled"
          },
          "404": {
            "description": "Room not found"
          },
          "422": {
            "description": "Blocked content type/extension",
            "too large": null,
            "or bad dimensions": null
          }
        }
      }
    },
    "/chat/rooms/{room_id}/media/{id}/complete": {
      "patch": {
        "tags": [
          "Chat"
        ],
        "summary": "Finalize an uploaded media file (uploader only)",
        "description": "Call after the S3 PUT succeeds. Images/files become `ready`\nimmediately; videos stay `pending` until transcoding completes.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Upload finalized",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "media": {
                      "$ref": "#/components/schemas/ChatMedia"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a room member"
          },
          "404": {
            "description": "Room or media not found (or not the caller's upload)"
          }
        }
      }
    },
    "/chat/rooms/{room_id}/media/{id}": {
      "delete": {
        "tags": [
          "Chat"
        ],
        "summary": "Discard an in-flight upload (uploader only)",
        "description": "Only unattached uploads can be removed — once a message references the media it is permanent.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Upload discarded"
          },
          "403": {
            "description": "Not a room member"
          },
          "404": {
            "description": "Room or media not found (or not the caller's upload)"
          },
          "422": {
            "description": "Media already attached to a sent message"
          }
        }
      }
    },
    "/chat/rooms/{room_id}/messages": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "Room message history (paged)",
        "description": "The history pager. Shares one window contract with\n`GET /chat/rooms/{id}` so both surfaces stay in lockstep. Omit every\ncursor for the newest page; pass `before_id` to walk strictly older\nhistory; pass `around_id` to land on a message of any age in one\nrequest (it wins over `before_id`). Opening the newest page marks the\nroom read - paging with a cursor deliberately does not, so walking\nhistory never bumps `last_read` forward.\n\n`has_more` is derived by over-fetching one row, so a room holding\nexactly `limit` messages correctly reports `has_more: false` rather\nthan offering a page that comes back empty.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "before_id",
            "in": "query",
            "description": "Return messages strictly older than this id (exclusive).",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "around_id",
            "in": "query",
            "description": "Jump-to-message window - roughly half the page at or before this id\nand half after it, in chronological order. Takes precedence over\n`before_id`.\n",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "include_thread_replies",
            "in": "query",
            "description": "Surface inline quote-replies in the timeline too. Default false\nkeeps the top-level-only contract thread-based clients expect.\n",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Message window",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "messages": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ChatMessage"
                      }
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "oldest_message_id": {
                      "type": "integer",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled or not a room member"
          },
          "404": {
            "description": "Room not found"
          }
        }
      },
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Post a message (asynchronous)",
        "description": "Queues the message for creation and Pusher broadcast (202). Provide\n`client_uuid` to reconcile the optimistic client bubble with the\nbroadcast. Attach pre-uploaded media via `media_ids` (max 10).\n`ack_type` marks the message as an Important Message or Read Receipt\nRequest (feature-gated; not allowed in channels; important messages\ncannot carry attachments).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "body": {
                    "type": "string",
                    "description": "Message text — required unless media_ids is present."
                  },
                  "parent_message_id": {
                    "type": "integer",
                    "description": "Message to quote/reply to (one-level inline quote)."
                  },
                  "client_uuid": {
                    "type": "string",
                    "description": "Client-generated UUID for dedup / optimistic UI."
                  },
                  "media_ids": {
                    "type": "array",
                    "maxItems": 10,
                    "items": {
                      "type": "integer"
                    },
                    "description": "Completed, unattached upload ids from POST /media."
                  },
                  "ack_type": {
                    "type": "string",
                    "enum": [
                      "important",
                      "read_receipt"
                    ],
                    "description": "Request explicit acknowledgement (feature-gated)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Message queued for delivery",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "queued"
                    },
                    "client_uuid": {
                      "type": "string",
                      "nullable": true
                    },
                    "room_id": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a room member"
          },
          "422": {
            "description": "Blank body+media / invalid media_ids / disallowed ack_type / bad parent"
          }
        }
      }
    },
    "/chat/rooms/{room_id}/messages/{id}": {
      "patch": {
        "tags": [
          "Chat"
        ],
        "summary": "Edit a message (author only, 15-minute window)",
        "description": "Edits the message body and broadcasts the update. Gated by the\n`allow_edit_chat_messages` business setting; attachment-only messages\nare not editable.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "properties": {
                  "body": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Message updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "$ref": "#/components/schemas/ChatMessage"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not author / editing disabled / outside edit window"
          },
          "404": {
            "description": "Message not found"
          },
          "422": {
            "description": "Blank body"
          }
        }
      },
      "delete": {
        "tags": [
          "Chat"
        ],
        "summary": "Delete a message (author or room admin, soft delete)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Message soft-deleted (returns the tombstoned message)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "$ref": "#/components/schemas/ChatMessage"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not the author and not a room admin"
          },
          "404": {
            "description": "Message not found"
          }
        }
      }
    },
    "/chat/rooms/{room_id}/messages/{message_id}/reactions": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "React to a message (idempotent)",
        "description": "Re-posting the same emoji returns 200 instead of duplicating.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "message_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emoji"
                ],
                "properties": {
                  "emoji": {
                    "type": "string",
                    "description": "One emoji from the supported set."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reaction already existed (idempotent repeat)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "reaction": {
                      "$ref": "#/components/schemas/ChatReaction"
                    },
                    "message": {
                      "$ref": "#/components/schemas/ChatMessage"
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Reaction added",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "reaction": {
                      "$ref": "#/components/schemas/ChatReaction"
                    },
                    "message": {
                      "$ref": "#/components/schemas/ChatMessage"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a room member"
          },
          "404": {
            "description": "Room or message not found"
          },
          "422": {
            "description": "Unsupported emoji"
          }
        }
      }
    },
    "/chat/rooms/{room_id}/messages/{message_id}/reactions/{id}": {
      "delete": {
        "tags": [
          "Chat"
        ],
        "summary": "Remove the caller's reaction",
        "description": "`id` accepts either the numeric reaction id or the emoji string itself\n(only the caller's own reaction is matched by emoji).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "message_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Reaction id or emoji string."
          }
        ],
        "responses": {
          "200": {
            "description": "Reaction removed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "$ref": "#/components/schemas/ChatMessage"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a room member or not the reaction owner"
          },
          "404": {
            "description": "Room",
            "message": null,
            "or reaction not found": null
          }
        }
      }
    },
    "/chat/rooms/{room_id}/messages/{message_id}/thread": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "Thread view (parent + replies)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "message_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Parent message with its replies (oldest first)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "parent": {
                      "$ref": "#/components/schemas/ChatMessage"
                    },
                    "replies": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ChatMessage"
                      }
                    },
                    "reply_count": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a room member"
          },
          "404": {
            "description": "Parent not found",
            "deleted": null,
            "or itself a reply": null
          }
        }
      }
    },
    "/chat/rooms/{room_id}/messages/{message_id}/acknowledge": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Acknowledge an important / read-receipt message (idempotent)",
        "description": "Explicit per-message acknowledgement for messages posted with an\n`ack_type`. A repeat call is a no-op (`noop: true`).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "message_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Acknowledged",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message_id": {
                      "type": "integer"
                    },
                    "acknowledged_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "workflow_participants_count": {
                      "type": "integer",
                      "description": "Eligible recipients who have acknowledged."
                    },
                    "ack_eligible_count": {
                      "type": "integer",
                      "description": "Total eligible recipients (excludes the author)."
                    },
                    "noop": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a room member"
          },
          "404": {
            "description": "Room or message not found"
          },
          "422": {
            "description": "Message does not require acknowledgement"
          }
        }
      }
    },
    "/chat/rooms/{room_id}/messages/{message_id}/acknowledgements": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "\"Read by X/Y\" acknowledgement report",
        "description": "Partitions eligible recipients into acknowledged and pending.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "message_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Acknowledgement report",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message_id": {
                      "type": "integer"
                    },
                    "ack_type": {
                      "type": "string",
                      "nullable": true,
                      "enum": [
                        "important",
                        "read_receipt",
                        null
                      ]
                    },
                    "acknowledged": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/ChatUserSummary"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "user_id": {
                                "type": "integer"
                              },
                              "acknowledged_at": {
                                "type": "string",
                                "format": "date-time"
                              }
                            }
                          }
                        ]
                      }
                    },
                    "pending": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/ChatUserSummary"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "user_id": {
                                "type": "integer"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a room member"
          },
          "404": {
            "description": "Room or message not found"
          }
        }
      }
    },
    "/chat/start_chat": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Find-or-create a conversation from a user selection",
        "description": "One selected user → DM; two or more → group. Reuses an existing room\nwith the identical member set unless `create_new_conv_always` is true.\nGroup name is auto-derived from member first names when omitted.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "selected_user_ids"
                ],
                "properties": {
                  "selected_user_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    }
                  },
                  "name": {
                    "type": "string",
                    "description": "Optional group name (ignored for DMs)."
                  },
                  "create_new_conv_always": {
                    "type": "boolean",
                    "description": "Skip dedup and always create a new room."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Room found or created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room": {
                      "$ref": "#/components/schemas/ChatRoom"
                    },
                    "created": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled or DMs/group chats disabled"
          },
          "422": {
            "description": "No users selected / self-only / users not in business"
          }
        }
      }
    },
    "/chat/users/search": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "User search for starting chats (recent-DM boosted)",
        "description": "Searches business users by name, email, employee id, job title, office\nlocation, and phone; users the caller recently messaged rank higher.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "active_only",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": true
            }
          },
          {
            "name": "roles[]",
            "in": "query",
            "description": "Role filter. Honoured ONLY for a caller who is manager-or-above in\nthe business; for anyone else it is silently ignored (the request\nstill succeeds and returns the default set, so pickers never\nbreak). Defaults to member/manager/admin/super_admin; guests are\nexcluded either way.\n",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page 1 additionally carries the caller's recent-DM contacts, prepended\nand de-duplicated against the paged window, so it returns more rows\nthan `per_page`. Size buffers off the returned array, not off\n`page x 50`.\n",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "all",
            "in": "query",
            "description": "Return up to 500 ranked matches in one response; `page` is ignored.\nNOT \"all matches without paging\" - that wording was wrong. When the\ndirectory is larger than the cap, `pagination.more` is true and the\nremainder is only reachable by narrowing `q`.\n",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ranked user matches",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string"
                          },
                          "text": {
                            "type": "string",
                            "description": "Same as name (typeahead convention)."
                          },
                          "email": {
                            "type": "string"
                          },
                          "avatar_url": {
                            "type": "string"
                          },
                          "meta": {
                            "type": "object",
                            "description": "Context metadata for the picker."
                          }
                        }
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "more": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled"
          }
        }
      }
    },
    "/chat/mentions": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "The caller's mentions inbox",
        "description": "Cross-room mentions of the caller (or a single room's with `room_id`),\nnewest first, cursor-paginated by mention id.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "before_id",
            "in": "query",
            "description": "Return mentions strictly older than this id (0 = newest page).",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 100
            }
          },
          {
            "name": "room_id",
            "in": "query",
            "description": "Scope to one conversation.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Mention page",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "mentions": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "message_id": {
                            "type": "integer"
                          },
                          "room_id": {
                            "type": "integer"
                          },
                          "user_id": {
                            "type": "integer",
                            "description": "The MENTIONED user (always the caller)."
                          },
                          "sender_id": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Author of the mentioning message."
                          },
                          "sender_name": {
                            "type": "string",
                            "nullable": true,
                            "description": "Author's display name, for the inbox row."
                          },
                          "sender_avatar_url": {
                            "type": "string",
                            "nullable": true,
                            "description": "Author's avatar (falls back to a generated initials image when no photo is set)."
                          },
                          "sender_avatar_updated_at": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Epoch seconds the author's avatar last changed — cache-bust token for URL-caching clients."
                          },
                          "body_excerpt": {
                            "type": "string"
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "oldest_mention_id": {
                      "type": "integer",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled"
          }
        }
      }
    },
    "/chat/search": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "Message search (full-text or semantic)",
        "description": "Searches message content across the caller's rooms. `mode=semantic`\nadds vector-embedding scoring when the business has semantic search\nenabled (falls back to FTS otherwise).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "mode",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "fts",
                "semantic"
              ],
              "default": "fts"
            }
          },
          {
            "name": "room_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "sender_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "date_from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "date_to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Search results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "room_id": {
                            "type": "integer"
                          },
                          "user_id": {
                            "type": "integer"
                          },
                          "body": {
                            "type": "string"
                          },
                          "parent_message_id": {
                            "type": "integer",
                            "nullable": true
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "snippet_html": {
                            "type": "string",
                            "nullable": true,
                            "description": "Highlighted match excerpt."
                          }
                        }
                      }
                    },
                    "mode": {
                      "type": "string",
                      "enum": [
                        "fts",
                        "semantic"
                      ]
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "query": {
                      "type": "string"
                    },
                    "semantic_available": {
                      "type": "boolean"
                    },
                    "semantic_attempted": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled"
          }
        }
      }
    },
    "/chat/search/senders": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "Sender autocomplete for the search filter",
        "description": "Users who have sent messages in the caller's active rooms.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Partial name/email. Below 2 characters NO senders are returned -\nthe endpoint does not fall back to an unfiltered alphabetical list,\nbecause a picker renders those rows as if they were matches.\n",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching senders",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "users": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ChatUserSummary"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled"
          }
        }
      }
    },
    "/chat/search/conversations": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "Find conversations by name or message content",
        "description": "Searches the caller's active conversations by group/channel name, DM\npartner name, or full message history (max 50 rooms). Message-content\nmatching uses the same full-text search as `/chat/search`, but returns\nroom objects so chat-list clients can render the matching conversation.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Conversations matching by name or message content",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conversations": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ChatRoom"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled"
          }
        }
      }
    },
    "/chat/settings": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "Business chat settings + caller admin flag",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Current settings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "settings": {
                      "$ref": "#/components/schemas/ChatSettings"
                    },
                    "admin": {
                      "type": "boolean",
                      "description": "Whether the caller can update settings."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled"
          }
        }
      },
      "patch": {
        "tags": [
          "Chat"
        ],
        "summary": "Update business chat settings (admin only)",
        "description": "Send any subset of keys; omitted keys are unchanged. Integer values are clamped to safe ranges.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChatSettings"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated settings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "settings": {
                      "$ref": "#/components/schemas/ChatSettings"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Not an admin or chat app disabled"
          },
          "422": {
            "description": "Validation failure"
          }
        }
      }
    },
    "/chat/presence/heartbeat": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Presence heartbeat",
        "description": "Marks the caller online; clients call this periodically.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled"
          }
        }
      }
    },
    "/chat/presence/online": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "Who is online",
        "description": "Either room-scoped (`room_id` — caller must be a member) or an explicit\n`user_ids` list (comma-separated, max 200).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "user_ids",
            "in": "query",
            "description": "Comma-separated user ids (max 200); used when room_id is omitted.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Online user ids",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "online_user_ids": {
                      "type": "array",
                      "items": {
                        "type": "integer"
                      }
                    },
                    "room_id": {
                      "type": "integer",
                      "description": "Echoed only in room_id mode."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled or not a member of the room"
          },
          "404": {
            "description": "Room not found"
          },
          "422": {
            "description": "More than 200 user_ids"
          }
        }
      }
    },
    "/chat/pusher/auth": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Authorize a Pusher channel subscription",
        "description": "Validates the channel against the caller's memberships and returns a\nsigned Pusher auth token. Supported channels: `private-room-<id>`,\n`private-user-inbox-<user_id>-business-<business_id>`, and\n`presence-room-<id>` (presence responses also include `channel_data`).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "channel_name",
                  "socket_id"
                ],
                "properties": {
                  "channel_name": {
                    "type": "string",
                    "example": "private-room-42"
                  },
                  "socket_id": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Subscription authorized",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "auth": {
                      "type": "string"
                    },
                    "channel_data": {
                      "type": "string",
                      "description": "JSON-encoded user info — presence channels only."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing channel_name or socket_id"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Unsupported channel",
            "not a member": null,
            "or cross-business mismatch": null
          },
          "503": {
            "description": "Pusher not configured on this server"
          }
        }
      }
    },
    "/chat/pusher/config": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "Pusher bootstrap config (legacy flat shape)",
        "description": "Backward-compatible alias — new clients should call GET /chat/config.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Pusher client config",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatPusherConfig"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled"
          }
        }
      }
    },
    "/chat/config": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "Unified client bootstrap (pusher + chat settings)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Combined config",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "pusher": {
                      "$ref": "#/components/schemas/ChatPusherConfig"
                    },
                    "chat_settings": {
                      "$ref": "#/components/schemas/ChatSettings"
                    },
                    "ask_ai_account": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ChatAskAiIdentity"
                        }
                      ],
                      "nullable": true,
                      "description": "The @Ask AI MENTION principal. Present only while the tenant's \"Enable @Ask AI in chat conversations\" setting (chat_settings.ask_ai_enabled) is on. null ⇒ do not offer the @Ask AI mention. Carries the extra history_search flag."
                    },
                    "ask_ai_chat": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ChatAskAiIdentity"
                        }
                      ],
                      "nullable": true,
                      "description": "The PINNED private Ask AI conversation (each user's own assistant chat). Gated on this USER's access to the Ask AI app — NOT on chat_settings.ask_ai_enabled, which controls only the in-room mention. null ⇒ do not show the pinned Ask AI chat. Read this key for the pinned entry; never infer it from ask_ai_account. `id` is null until the assistant principal has been provisioned — render the name/avatar fallbacks as usual."
                    },
                    "reactions": {
                      "type": "array",
                      "description": "Server-driven reaction set (also at GET /chat/reactions).",
                      "items": {
                        "$ref": "#/components/schemas/ChatReaction"
                      }
                    },
                    "klipy": {
                      "type": "object",
                      "description": "GIF-picker (KLIPY) bootstrap. enabled=false ⇒ hide the picker.",
                      "properties": {
                        "api_key": {
                          "type": "string",
                          "nullable": true
                        },
                        "base_url": {
                          "type": "string"
                        },
                        "enabled": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled"
          }
        }
      }
    },
    "/rfp_desk/answers": {
      "get": {
        "tags": [
          "RFP Desk"
        ],
        "summary": "List answer-library entries",
        "description": "Approved, reusable answers to RFP questions. Requires the RFP Desk contributor (or admin) grant — the same gate as the Answer Library tab.\nFilter by `category`, `status` (`active`, `archived`, `stale` — not reviewed in 12 months) and a free-text `q` over question and answer text.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "category",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "technical",
                "compliance",
                "commercial",
                "delivery",
                "other"
              ]
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "archived",
                "stale"
              ]
            }
          },
          {
            "name": "mine",
            "in": "query",
            "description": "Only answers the caller is named owner of",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Number of records per page",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated answers",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RfpAnswer"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Standard pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items across all pages",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 15
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Number of items per page",
                          "example": 10
                        },
                        "has_next": {
                          "type": "boolean",
                          "description": "Whether there is a next page",
                          "example": true
                        },
                        "has_prev": {
                          "type": "boolean",
                          "description": "Whether there is a previous page",
                          "example": false
                        },
                        "segment_counts": {
                          "type": "object",
                          "description": "Per-segment row counts for the screen's filter-tab badges (exact, not capped at per_page) so a client can label every tab from the first response without fetching each one. Each count equals the matching status_type filter's total_count by construction. Keys depend on the feed (the two screens have different tabs):\n  - Personal feed (team omitted/false): active, under_review, completed.\n  - Team feed (team=true): all, in_progress, completed, failed, overdue.\nPresent on both feeds.",
                          "additionalProperties": {
                            "type": "integer"
                          },
                          "example": {
                            "active": 7,
                            "under_review": 0,
                            "completed": 23
                          }
                        }
                      }
                    },
                    "total_count": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Insufficient permissions to access this resource",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine-readable error code",
                          "example": "invalid_request"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human-readable error message",
                          "example": "The request could not be processed"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context",
                          "additionalProperties": true
                        },
                        "request_id": {
                          "type": "string",
                          "description": "Unique request identifier for debugging",
                          "example": "req_abc123"
                        }
                      }
                    },
                    "success": {
                      "type": "boolean",
                      "example": false
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "RFP Desk"
        ],
        "summary": "Add an answer to the library",
        "description": "Creates an approved answer. Questions are unique per workspace (case-insensitive); a duplicate returns 422.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RfpAnswerInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "answer": {
                      "$ref": "#/components/schemas/RfpAnswer"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Request cannot be processed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine-readable error code",
                          "example": "invalid_request"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human-readable error message",
                          "example": "The request could not be processed"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context",
                          "additionalProperties": true
                        },
                        "request_id": {
                          "type": "string",
                          "description": "Unique request identifier for debugging",
                          "example": "req_abc123"
                        }
                      }
                    },
                    "success": {
                      "type": "boolean",
                      "example": false
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/rfp_desk/answers/{id}": {
      "get": {
        "tags": [
          "RFP Desk"
        ],
        "summary": "Get one answer",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The answer",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "answer": {
                      "$ref": "#/components/schemas/RfpAnswer"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Resource not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine-readable error code",
                          "example": "invalid_request"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human-readable error message",
                          "example": "The request could not be processed"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context",
                          "additionalProperties": true
                        },
                        "request_id": {
                          "type": "string",
                          "description": "Unique request identifier for debugging",
                          "example": "req_abc123"
                        }
                      }
                    },
                    "success": {
                      "type": "boolean",
                      "example": false
                    }
                  }
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "RFP Desk"
        ],
        "summary": "Update an answer",
        "description": "Any subset of the input fields. Setting `active` to true reactivates an archived answer.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RfpAnswerInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "answer": {
                      "$ref": "#/components/schemas/RfpAnswer"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Request cannot be processed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine-readable error code",
                          "example": "invalid_request"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human-readable error message",
                          "example": "The request could not be processed"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context",
                          "additionalProperties": true
                        },
                        "request_id": {
                          "type": "string",
                          "description": "Unique request identifier for debugging",
                          "example": "req_abc123"
                        }
                      }
                    },
                    "success": {
                      "type": "boolean",
                      "example": false
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/rfp_desk/answers/{id}/review": {
      "post": {
        "tags": [
          "RFP Desk"
        ],
        "summary": "Mark an answer as reviewed",
        "description": "Confirms the answer is still true without editing it — resets the 12-month staleness clock.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Reviewed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "answer": {
                      "$ref": "#/components/schemas/RfpAnswer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/rfp_desk/answers/{id}/archive": {
      "post": {
        "tags": [
          "RFP Desk"
        ],
        "summary": "Retire an answer",
        "description": "Sets `active` to false. The answer stops being offered to drafters; sections that already reused it keep their provenance. There is no delete over the API.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Archived",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "answer": {
                      "$ref": "#/components/schemas/RfpAnswer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/rfp_desk/responses": {
      "get": {
        "tags": [
          "RFP Desk"
        ],
        "summary": "List RFP responses",
        "description": "Responses the caller could open in the browser: the participant floor (responsible, collaborator, section owner, creator) plus the workspace's who-can-view-all-responses setting. Filter by `status`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "on_hold",
                "in_review",
                "approved",
                "submitted",
                "declined"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Number of records per page",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated responses",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RfpResponseSummary"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Standard pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items across all pages",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 15
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Number of items per page",
                          "example": 10
                        },
                        "has_next": {
                          "type": "boolean",
                          "description": "Whether there is a next page",
                          "example": true
                        },
                        "has_prev": {
                          "type": "boolean",
                          "description": "Whether there is a previous page",
                          "example": false
                        },
                        "segment_counts": {
                          "type": "object",
                          "description": "Per-segment row counts for the screen's filter-tab badges (exact, not capped at per_page) so a client can label every tab from the first response without fetching each one. Each count equals the matching status_type filter's total_count by construction. Keys depend on the feed (the two screens have different tabs):\n  - Personal feed (team omitted/false): active, under_review, completed.\n  - Team feed (team=true): all, in_progress, completed, failed, overdue.\nPresent on both feeds.",
                          "additionalProperties": {
                            "type": "integer"
                          },
                          "example": {
                            "active": 7,
                            "under_review": 0,
                            "completed": 23
                          }
                        }
                      }
                    },
                    "total_count": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/rfp_desk/responses/{id}": {
      "get": {
        "tags": [
          "RFP Desk"
        ],
        "summary": "Get a response with its sections",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "only_gaps",
            "in": "query",
            "description": "Return only sections still needing input",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The response and its sections in order",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "response": {
                      "$ref": "#/components/schemas/RfpResponse"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Resource not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine-readable error code",
                          "example": "invalid_request"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human-readable error message",
                          "example": "The request could not be processed"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context",
                          "additionalProperties": true
                        },
                        "request_id": {
                          "type": "string",
                          "description": "Unique request identifier for debugging",
                          "example": "req_abc123"
                        }
                      }
                    },
                    "success": {
                      "type": "boolean",
                      "example": false
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/rfp_desk/responses/{id}/sections/{section_id}": {
      "patch": {
        "tags": [
          "RFP Desk"
        ],
        "summary": "Answer a section",
        "description": "Writes the section's draft as a human answer — the same rules as the `resolve_section` agent tool: an approved or submitted response refuses (409), a draft still carrying a `[NEEDS INPUT: …]` placeholder leaves the gap open, a changed draft is marked as human-grounded and its review mark is cleared, and the routed task (if any) closes when the gap closes. `save_to_library` also banks the answer as a library entry (contributors only; skipped for narrative sections and open gaps).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "section_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "draft"
                ],
                "properties": {
                  "draft": {
                    "type": "string"
                  },
                  "save_to_library": {
                    "type": "boolean",
                    "default": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated section",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "section": {
                      "$ref": "#/components/schemas/RfpSection"
                    },
                    "remaining_gaps": {
                      "type": "integer"
                    },
                    "still_needs_input": {
                      "type": "boolean"
                    },
                    "saved_to_library": {
                      "type": "boolean"
                    },
                    "library_note": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The response is approved or submitted — reopen it in RFP Desk first"
          },
          "422": {
            "description": "Request cannot be processed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine-readable error code",
                          "example": "invalid_request"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human-readable error message",
                          "example": "The request could not be processed"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context",
                          "additionalProperties": true
                        },
                        "request_id": {
                          "type": "string",
                          "description": "Unique request identifier for debugging",
                          "example": "req_abc123"
                        }
                      }
                    },
                    "success": {
                      "type": "boolean",
                      "example": false
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/rfp_desk/responses/{id}/transition": {
      "post": {
        "tags": [
          "RFP Desk"
        ],
        "summary": "Move a response to another status",
        "description": "Same transition map and approval gate as the web app. Approving, submitting, declining or reopening a locked response needs the responsible person or an app admin (403 otherwise). Approval needs every section answered and reviewed and every required deliverable ticked (422 with the reason otherwise). Declining withdraws routed questions from people's task lists.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "status"
                ],
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "draft",
                      "on_hold",
                      "in_review",
                      "approved",
                      "submitted",
                      "declined"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The response after the move",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "response": {
                      "$ref": "#/components/schemas/RfpResponseSummary"
                    },
                    "withdrawn_tasks": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Insufficient permissions to access this resource",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine-readable error code",
                          "example": "invalid_request"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human-readable error message",
                          "example": "The request could not be processed"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context",
                          "additionalProperties": true
                        },
                        "request_id": {
                          "type": "string",
                          "description": "Unique request identifier for debugging",
                          "example": "req_abc123"
                        }
                      }
                    },
                    "success": {
                      "type": "boolean",
                      "example": false
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Request cannot be processed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine-readable error code",
                          "example": "invalid_request"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human-readable error message",
                          "example": "The request could not be processed"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context",
                          "additionalProperties": true
                        },
                        "request_id": {
                          "type": "string",
                          "description": "Unique request identifier for debugging",
                          "example": "req_abc123"
                        }
                      }
                    },
                    "success": {
                      "type": "boolean",
                      "example": false
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/rfp_desk/assessments": {
      "get": {
        "tags": [
          "RFP Desk"
        ],
        "summary": "List bid/no-bid assessments",
        "description": "Completed rfp_fit assessments the caller may read — all of them for an RFP Desk app admin, otherwise the ones they ran. Filter by `recommendation`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "recommendation",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "go",
                "conditional",
                "no_bid"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Number of records per page",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated assessments",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RfpAssessmentSummary"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Standard pagination metadata",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Total number of items across all pages",
                          "example": 150
                        },
                        "total_pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 15
                        },
                        "current_page": {
                          "type": "integer",
                          "description": "Current page number",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Number of items per page",
                          "example": 10
                        },
                        "has_next": {
                          "type": "boolean",
                          "description": "Whether there is a next page",
                          "example": true
                        },
                        "has_prev": {
                          "type": "boolean",
                          "description": "Whether there is a previous page",
                          "example": false
                        },
                        "segment_counts": {
                          "type": "object",
                          "description": "Per-segment row counts for the screen's filter-tab badges (exact, not capped at per_page) so a client can label every tab from the first response without fetching each one. Each count equals the matching status_type filter's total_count by construction. Keys depend on the feed (the two screens have different tabs):\n  - Personal feed (team omitted/false): active, under_review, completed.\n  - Team feed (team=true): all, in_progress, completed, failed, overdue.\nPresent on both feeds.",
                          "additionalProperties": {
                            "type": "integer"
                          },
                          "example": {
                            "active": 7,
                            "under_review": 0,
                            "completed": 23
                          }
                        }
                      }
                    },
                    "total_count": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/rfp_desk/assessments/{id}": {
      "get": {
        "tags": [
          "RFP Desk"
        ],
        "summary": "Get one assessment",
        "description": "The verdict, weighted breakdown, extracted requirements and any hard gate (unmet mandatory requirement or walk-away hit). The RFP's own text is never returned.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The assessment",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "assessment": {
                      "$ref": "#/components/schemas/RfpAssessment"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Resource not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine-readable error code",
                          "example": "invalid_request"
                        },
                        "message": {
                          "type": "string",
                          "description": "Human-readable error message",
                          "example": "The request could not be processed"
                        },
                        "details": {
                          "type": "object",
                          "description": "Additional error context",
                          "additionalProperties": true
                        },
                        "request_id": {
                          "type": "string",
                          "description": "Unique request identifier for debugging",
                          "example": "req_abc123"
                        }
                      }
                    },
                    "success": {
                      "type": "boolean",
                      "example": false
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/ai_notepad/config": {
      "get": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "AI Notepad feature configuration for the caller's business",
        "description": "The tenant's notepad settings — which AI features are on, the upload\nceiling, and creation defaults. Doubles as the namespace's health/access\nprobe: a business without the app enabled gets the standard JSON 403.\n\nRealtime credentials are deliberately NOT served here; clients reuse\n`GET /api/v1/chat/config` for Pusher.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Feature configuration",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ai_notepad": {
                      "type": "object",
                      "properties": {
                        "enabled": {
                          "type": "boolean"
                        },
                        "settings": {
                          "type": "object",
                          "description": "Tenant feature flags and defaults. Additive over time — treat unknown keys as forward compatibility.",
                          "additionalProperties": true,
                          "properties": {
                            "ai_enabled": {
                              "type": "boolean"
                            },
                            "audio_overview_enabled": {
                              "type": "boolean"
                            },
                            "meeting_chat_enabled": {
                              "type": "boolean"
                            },
                            "output_formats_enabled": {
                              "type": "boolean"
                            },
                            "content_revision_enabled": {
                              "type": "boolean"
                            },
                            "meeting_comparison_enabled": {
                              "type": "boolean"
                            },
                            "sharing_enabled": {
                              "type": "boolean"
                            },
                            "automations_enabled": {
                              "type": "boolean"
                            },
                            "accountability_digest_enabled": {
                              "type": "boolean"
                            },
                            "calendar_stub_enabled": {
                              "type": "boolean"
                            },
                            "practice_recommendations_enabled": {
                              "type": "boolean"
                            },
                            "max_upload_size_mb": {
                              "type": "integer"
                            },
                            "default_meeting_source": {
                              "type": "string"
                            },
                            "default_template_id": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/NotepadForbidden"
          }
        }
      }
    },
    "/ai_notepad/pusher/auth": {
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Authorize a Pusher subscription for a notepad channel",
        "description": "Signs a subscription to one of the two notepad channels:\n\n  - `private-ai-notepad-meeting-{id}` — per-meeting: status, artifacts,\n    transcript and audio-overview events. Requires the caller to be able\n    to view that meeting.\n  - `private-user-ai-notepad-{userId}-business-{businessId}` — the\n    caller's own inbox channel (meeting created/updated/deleted, action\n    item assigned). Requires the ids to be the caller's own.\n\nThe response is the raw Pusher auth payload, NOT wrapped in an envelope,\nso it can be handed straight to the Pusher client.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "socket_id",
                  "channel_name"
                ],
                "properties": {
                  "socket_id": {
                    "type": "string"
                  },
                  "channel_name": {
                    "type": "string",
                    "description": "One of the two notepad channel shapes above."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Pusher auth payload (returned direct, un-enveloped)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "auth": {
                      "type": "string"
                    },
                    "channel_data": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/NotepadError"
          },
          "403": {
            "description": "Not a channel this caller may subscribe to",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/ai_notepad/preferences": {
      "get": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "The caller's own notepad preferences",
        "description": "EFFECTIVE values — the user's stored preferences resolved against the\ntenant policy — plus a `policy` block describing the ceiling the admin\nset. `policy.admin_owned` names the keys a user cannot override.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Effective preferences and the tenant policy",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadPreferences"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/NotepadForbidden"
          }
        }
      },
      "patch": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Update the caller's own notepad preferences",
        "description": "Stores ONLY the keys present in the request; omitted keys are left\nalone rather than reset. An admin-owned key is refused rather than\nsilently ignored. Responds with the same effective shape as GET.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "meeting_detection_enabled": {
                    "type": "boolean"
                  },
                  "auto_capture": {
                    "type": "string"
                  },
                  "default_notebook_id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "default_language": {
                    "type": "string",
                    "nullable": true
                  },
                  "retain_audio_days": {
                    "type": "integer"
                  },
                  "notify_on_ready": {
                    "type": "boolean",
                    "description": "The \"your meeting notes are ready\" inbox message."
                  },
                  "default_template_id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "capture_consent_granted": {
                    "type": "boolean"
                  },
                  "meeting_reminders_enabled": {
                    "type": "boolean"
                  },
                  "meeting_reminder_lead_minutes": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated effective preferences",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadPreferences"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/NotepadForbidden"
          },
          "422": {
            "$ref": "#/components/responses/NotepadValidationError"
          }
        }
      }
    },
    "/ai_notepad/meetings": {
      "get": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "List the caller's meetings and notes",
        "description": "Everything the caller can view, newest first. Each row carries\ndenormalized action-item counts and an `artifacts_status` so a list\nrender needs no per-row fan-out.\n\n**Two paging modes, and they are exclusive.** Without `sort` the list is\nkeyset-paginated on `cursor`; WITH `sort` it switches to offset paging on\n`page`, because a keyset cursor over a non-id ordering skips and\nduplicates rows. `meta.pagination.mode` says which mode answered and\n`meta.pagination.ignored` names the paging param that was discarded, so\na client never loops on page 1 in silence.\n\n`view` chooses the LIST (live notes, or Recently Deleted); `filter` is\nthe chip row stacked on top of it. Deleted rows are excluded unless\n`view=trash`.\n\nEvery vocabulary param (`view`, `filter`, `status`, `sort`) REFUSES an\nunrecognized value with 422 and the offending value echoed — it is never\nanswered with a list computed under a different question. The one\nexception is `status=capturing`, which is in the client vocabulary but\nhas no server state yet, so it legitimately returns an empty list.\n\n`meta.filters` echoes the APPLIED narrowing, so a client that reuses a\nstale `cursor` under changed filters can tell.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `meta.next_cursor` from the previous page. Keyset mode only — discarded (and reported in `meta.pagination.ignored`) when `sort` is sent.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "1-based offset page. Offset mode only — discarded (and reported in `meta.pagination.ignored`) when `sort` is absent.",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Switches the endpoint from keyset to offset paging. Absent means the default id-descending keyset order.",
            "schema": {
              "type": "string",
              "enum": [
                "edited",
                "created",
                "title"
              ]
            }
          },
          {
            "name": "view",
            "in": "query",
            "description": "Which list to read. `all` is the default and a legitimate no-op; `trash` is Recently Deleted. The Meetings / Notes / Shared narrowings are `filter`, not spellings of this param.",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "trash"
              ],
              "default": "all"
            }
          },
          {
            "name": "filter",
            "in": "query",
            "description": "The chip row — what a note IS, as opposed to `status`, which is what has happened to it. `deleted` is deliberately absent: Recently Deleted is `view=trash`.",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "meetings",
                "notes",
                "shared"
              ]
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Client-vocabulary status filter. `capturing` is accepted and returns an empty list — no row this server can serialize will ever read it.",
            "schema": {
              "type": "string",
              "enum": [
                "processing",
                "completed",
                "failed",
                "capturing"
              ]
            }
          },
          {
            "name": "notebook_id",
            "in": "query",
            "description": "Must be a notebook the caller can view; a deleted, foreign or never-existed id is a 422, not an empty list.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Case-insensitive substring search across the meeting title, AI summary, transcript and the user's own notes — the same four columns the web search page reads, so the desktop client and the browser answer the same question. No minimum query length.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated meeting list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "meetings": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NotepadMeetingSummary"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/NotepadListMeta"
                    },
                    "page": {
                      "type": "integer",
                      "description": "Offset mode only (`sort` sent) — the shipped root-level twin of `meta.page`. Absent in keyset mode."
                    },
                    "has_more": {
                      "type": "boolean",
                      "description": "Offset mode only — the shipped root-level twin of `meta.has_more`. Absent in keyset mode."
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/NotepadForbidden"
          },
          "422": {
            "description": "An unrecognized `view` / `filter` / `status` / `sort` value, or a `notebook_id` the caller cannot browse. `error.code` is one of `unsupported_view`, `unsupported_filter`, `unsupported_status`, `unsupported_sort`, `notebook_not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Create a meeting or a note",
        "description": "`source` takes the CLIENT vocabulary. `paste` and `quick_note` carry\ntheir content inline and enqueue the AI pipeline immediately; `upload`\ncreates the row first and the recording is attached by\n`POST /meetings/{id}/audio`; `system_audio` / `voice_note` are the\nlive-capture sources.\n\nHonours `Idempotency-Key`, so a retried create returns the original\nmeeting rather than a duplicate.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "source"
                ],
                "properties": {
                  "source": {
                    "type": "string",
                    "enum": [
                      "system_audio",
                      "voice_note",
                      "upload",
                      "paste",
                      "quick_note"
                    ]
                  },
                  "title": {
                    "type": "string"
                  },
                  "text": {
                    "type": "string",
                    "description": "Body for `paste` / `quick_note`."
                  },
                  "note": {
                    "type": "string",
                    "description": "Extra user notes carried alongside a pasted transcript."
                  },
                  "notebook_id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "template_id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "occurred_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Defaults to now when ABSENT. A value that is present but unparseable or outside the supported year range is a 422, never a silent substitution of now."
                  },
                  "language": {
                    "type": "string",
                    "nullable": true
                  },
                  "duration_sec": {
                    "type": "integer",
                    "description": "Client-measured capture length in whole seconds, for the desktop dictation flow where there is no audio for the pipeline to measure. Non-positive values are ignored."
                  },
                  "calendar_event_id": {
                    "type": "string",
                    "nullable": true,
                    "description": "The occurrence this note is being taken for, as handed back by `GET /meetings/upcoming`. Send it with `calendar_provider` so the note the desktop makes and the note the web makes for one meeting are the same row."
                  },
                  "calendar_provider": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A note for this `calendar_event_id` already existed, so nothing was created and the existing one is handed back. Same body as the 201; `processing.reason` is `already_exists`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadCreateResponse"
                }
              }
            }
          },
          "201": {
            "description": "The created meeting",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadCreateResponse"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/NotepadForbidden"
          },
          "422": {
            "$ref": "#/components/responses/NotepadValidationError"
          }
        }
      }
    },
    "/ai_notepad/meetings/upcoming": {
      "get": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Calendar meetings starting soon",
        "description": "The pre-meeting feed the desktop client raises its \"your meeting starts\nin N minutes — start AI Notepad?\" prompt from. Same resolver the web and\n`/m/` prompts poll, so the surfaces cannot disagree.\n\n`meta.enabled` reflects the tenant's calendar-stub setting;\n`meta.next_poll_in_seconds` is the server telling the client how long to\nwait before asking again.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Upcoming calendar meetings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "meetings": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NotepadUpcomingMeeting"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "One key set in both branches, with an explicit null or zero where a value does not apply — a caller with the prompt switched off is answered `enabled: false` and a 200, never a 403.",
                      "properties": {
                        "enabled": {
                          "type": "boolean"
                        },
                        "lead_minutes": {
                          "type": "integer",
                          "nullable": true
                        },
                        "next_poll_in_seconds": {
                          "type": "integer",
                          "nullable": true,
                          "description": "How long to wait before asking again. Null means stand down."
                        },
                        "count": {
                          "type": "integer",
                          "description": "Rows returned in this response."
                        },
                        "max_results": {
                          "type": "integer",
                          "description": "The server-side bound on the feed. There is no cursor: this is a rolling window the client re-polls, not a list it pages through."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/NotepadForbidden"
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}": {
      "get": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Meeting detail",
        "description": "The list shape plus everything the detail pane needs: the note body in\nboth formats, capability flags, a signed expiring recording URL, the\naudio-overview state, the source document's web URL, and the public\nshare-link state.\n\n`share_link` is always present and is `null` until a public link exists.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "responses": {
          "200": {
            "description": "Meeting detail",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadMeetingDetail"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/NotepadForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Delete a meeting",
        "description": "Moves the meeting into Recently Deleted, recoverable for 30 days by\n`POST /meetings/{id}/restore`; a scheduled sweep purges it after that.\nOnly the creator or an `owner` collaborator may delete — shared editors\nand viewers cannot.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted. `deleted` is true and `deleted_at` starts the purge countdown.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadMutationAck"
                }
              }
            }
          },
          "403": {
            "description": "The caller is not the creator or an `owner` collaborator, or the note is already in Recently Deleted (`meeting_deleted`) — re-deleting would restart the 30-day purge clock on a note the user is trying to recover.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "422": {
            "description": "The row vanished between the load and the write (`meeting_delete_failed`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}/restore": {
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Restore a meeting from Recently Deleted",
        "description": "Takes the same permission as deleting — it is the inverse of it, not a lesser action. Answers the shared terse ack with `deleted_at` back to null, so a client merging it into its cached row drops the purge countdown and knows where the restored note sorts.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "responses": {
          "200": {
            "description": "Restored, with `deleted` back to false",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadMutationAck"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/NotepadForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "422": {
            "description": "The note is not in Recently Deleted (`meeting_not_deleted`), or the row vanished under the request (`meeting_restore_failed`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}/pin": {
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Pin a meeting to the top of the list",
        "description": "Pinned rows sort above the chosen sort order rather than instead of it.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "responses": {
          "200": {
            "description": "Pin state, in the shared terse-ack shape",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadMutationAck"
                }
              }
            }
          },
          "403": {
            "description": "Only the note's owner may pin it, or the note is in Recently Deleted (`meeting_deleted`) — pinning writes the column every viewer's list is ordered by.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "422": {
            "description": "The row vanished between the load and the write (`meeting_pin_failed`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Unpin a meeting",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "responses": {
          "200": {
            "description": "Pin state, with `pinned_at` cleared",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadMutationAck"
                }
              }
            }
          },
          "403": {
            "description": "Only the note's owner may unpin it, or the note is in Recently Deleted (`meeting_deleted`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "422": {
            "description": "The row vanished between the load and the write (`meeting_unpin_failed`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}/notebook": {
      "patch": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "File a meeting into a notebook, or take it out",
        "description": "A blank / null `notebook_id` means \"take it out of its notebook\". A\nnarrow member action rather than a broad meeting PATCH, so that title\nand visibility cannot be silently accepted and dropped.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "notebook_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Null or blank removes the meeting from its notebook."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated meeting",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "meeting": {
                      "$ref": "#/components/schemas/NotepadMeetingEntity"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "422": {
            "$ref": "#/components/responses/NotepadValidationError"
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}/notes": {
      "patch": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Replace the note body (the editor's autosave target)",
        "description": "Called repeatedly on a debounce, so it is cheap and idempotent.\n\nSend `notes_html`. It is the canonical body, and it WINS whenever the\nkey is present — so a client sending rich text alongside a stale\nmarkdown copy of it keeps the rich text.\n\n`notes` (and its `user_notes_markdown` alias) are DEPRECATED and\naccepted only until the pre-flip desktop build has rolled out. Markdown\nwas retired as a write format because a client round-tripping a body\nthrough it silently flattens tables, highlights and images; a client\nthat only ever had markdown is not round-tripping anything, which is why\nthe fallback is safe in the meantime. Do not write new clients against\nit.\n\nA request carrying no body key at all answers 422 `notes_required`, and\nso does an explicitly null `notes_html` — a dropped or lost key must\nnever be read as \"erase the note\". An explicit `\"\"` clears the body,\nwhich is a real edit.\n\nThe response echoes BOTH formats: the server derives whichever one you\ndid not send, and the markdown copy is what the AI pipeline and search\nread.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "One body key is required. `notes_html` for every current client; the markdown pair is the deprecated pre-flip shape.",
                "properties": {
                  "notes_html": {
                    "type": "string",
                    "description": "The rich-text body. `\"\"` clears it. Wins over the pair below."
                  },
                  "notes": {
                    "type": "string",
                    "deprecated": true,
                    "description": "Markdown body, pre-flip clients only. The server derives the HTML from it."
                  },
                  "user_notes_markdown": {
                    "type": "string",
                    "deprecated": true,
                    "description": "Alias of `notes`."
                  }
                },
                "anyOf": [
                  {
                    "required": [
                      "notes_html"
                    ]
                  },
                  {
                    "required": [
                      "notes"
                    ]
                  },
                  {
                    "required": [
                      "user_notes_markdown"
                    ]
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The saved body in both formats",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "meeting_id": {
                      "type": "integer"
                    },
                    "notes": {
                      "type": "string",
                      "nullable": true,
                      "description": "Server-derived markdown twin."
                    },
                    "notes_html": {
                      "type": "string",
                      "nullable": true
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "processing",
                        "completed",
                        "failed"
                      ],
                      "description": "The server may have flipped this — writing to a failed note resets it."
                    },
                    "saved_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/NotepadForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "422": {
            "description": "`not_a_note` (only a note's body is directly editable — use the\nsection editor for AI output), `notes_required`, or\n`content_too_long`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}/transcript": {
      "get": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "The meeting transcript, in ordered segments",
        "description": "`ready: false` with an empty `segments` array is a normal \"not\ntranscribed yet\" answer, NOT an error — the client waits for the\nrealtime `transcript:final` event.\n\nSegments are derived from the stored transcript at request time, so\n`start_ms`, `end_ms` and `confidence` are currently always null; there is\nno per-segment timing yet.\n\n`status` here is the MEETING vocabulary while the sibling artifacts read\nputs the ARTIFACTS vocabulary under that same key, and the two COLLIDE on\n`failed`. `meeting_status` and `artifacts_status` are the unambiguous\npair, present on all three of this meeting's reads and always meaning the\nsame thing — prefer them.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 200,
              "minimum": 1,
              "maximum": 500
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `meta.next_cursor` (a segment `seq`) from the previous page.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "full",
            "in": "query",
            "description": "Return every segment in one response, ignoring `limit`.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Transcript segments (possibly not ready yet)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "meeting_id": {
                      "type": "integer"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "processing",
                        "completed",
                        "failed"
                      ],
                      "description": "The MEETING vocabulary. Ambiguous by name across this meeting's three reads — see `meeting_status`."
                    },
                    "meeting_status": {
                      "type": "string",
                      "enum": [
                        "processing",
                        "completed",
                        "failed"
                      ],
                      "description": "The meeting's own lifecycle, under a name that cannot be confused with the artifacts one."
                    },
                    "artifacts_status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "ready",
                        "failed"
                      ],
                      "description": "Whether the AI output exists yet. `pending` means show a skeleton, not an error."
                    },
                    "ready": {
                      "type": "boolean",
                      "description": "False until a transcript exists."
                    },
                    "speakers": {
                      "type": "array",
                      "description": "The speaker roster. Every element carries an `id` (`s1`, `s2`, …) that the segments' `speaker` resolves against — derived from the element's position when the stored roster has none.",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      }
                    },
                    "segments": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NotepadTranscriptSegment"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "full": {
                          "type": "boolean"
                        },
                        "limit": {
                          "type": "integer",
                          "description": "Page SIZE, in both modes."
                        },
                        "count": {
                          "type": "integer",
                          "description": "Segments actually returned."
                        },
                        "has_more": {
                          "type": "boolean"
                        },
                        "next_cursor": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Always null in `full` mode, which discards the cursor — read the rest with the paged mode, which honours it and terminates."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}/artifacts": {
      "get": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "The AI output — summary and insight sections",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "responses": {
          "200": {
            "description": "Artifacts, with a status the client renders skeletons from",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadArtifacts"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}/regenerate": {
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Re-run the AI pipeline for a meeting's artifacts",
        "description": "Returns 202 with the meeting back in `processing`; completion arrives via the realtime `artifacts:ready` event.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "target": {
                    "type": "string",
                    "enum": [
                      "summary",
                      "insights",
                      "action_items",
                      "all"
                    ],
                    "default": "all"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Regeneration enqueued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "meeting": {
                      "$ref": "#/components/schemas/NotepadMeeting"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "422": {
            "$ref": "#/components/responses/NotepadError"
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}/convert": {
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Convert a plain note into a meeting note",
        "description": "Runs the note's own text through the AI pipeline. Refused for an empty note — its text is what a meeting note is summarized from.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "responses": {
          "202": {
            "description": "Conversion enqueued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "meeting": {
                      "$ref": "#/components/schemas/NotepadMeetingEntity"
                    },
                    "truncated_fields": {
                      "type": "array",
                      "description": "Which body fields the server's length ceiling cut — the same key and meaning the create carries. Always an array so the key set is stable; empty for `mode=notes`, which pastes nothing.",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "422": {
            "$ref": "#/components/responses/NotepadError"
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}/transcribe_text": {
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Submit or replace a pasted transcript",
        "description": "The text path — no ASR. Only valid for a `paste`-sourced meeting; a quick note or a recording-backed meeting is refused 422.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "text"
                ],
                "properties": {
                  "text": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Pipeline enqueued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "meeting": {
                      "$ref": "#/components/schemas/NotepadMeeting"
                    },
                    "processing": {
                      "type": "object",
                      "description": "Whether the pipeline was actually enqueued — the same shape the create answers with, so a client can stop rendering a progress state for a job that was never started.",
                      "properties": {
                        "started": {
                          "type": "boolean"
                        },
                        "reason": {
                          "type": "string"
                        }
                      }
                    },
                    "truncated_fields": {
                      "type": "array",
                      "description": "Which body fields the server's length ceiling cut. Always an array; empty is the normal case.",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "422": {
            "$ref": "#/components/responses/NotepadError"
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}/audio": {
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Attach a recording (direct multipart upload)",
        "description": "The small-file transport. Enforces the tenant's size ceiling and the\nallowed formats, then enqueues the AI pipeline. Use the resumable\n`audio/init` + `audio/complete` pair for large files.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Attached and pipeline enqueued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "meeting": {
                      "$ref": "#/components/schemas/NotepadMeeting"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "Larger than the tenant's upload ceiling",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported audio format",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/NotepadError"
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}/audio/init": {
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Begin a resumable recording upload",
        "description": "Returns a direct-upload target the client PUTs the file to, then finalizes with `audio/complete`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "filename",
                  "byte_size",
                  "content_type"
                ],
                "properties": {
                  "filename": {
                    "type": "string"
                  },
                  "byte_size": {
                    "type": "integer"
                  },
                  "content_type": {
                    "type": "string"
                  },
                  "checksum": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Upload target",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "meeting_id": {
                      "type": "integer"
                    },
                    "upload": {
                      "type": "object",
                      "properties": {
                        "signed_blob_id": {
                          "type": "string"
                        },
                        "url": {
                          "type": "string"
                        },
                        "byte_size": {
                          "type": "integer"
                        },
                        "headers": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "Larger than the tenant's upload ceiling",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported audio format",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/NotepadError"
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}/audio/complete": {
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Finalize a resumable recording upload",
        "description": "Attaches the uploaded blob to the meeting and enqueues the AI pipeline.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "signed_blob_id"
                ],
                "properties": {
                  "signed_blob_id": {
                    "type": "string",
                    "description": "From the `audio/init` response."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Attached and pipeline enqueued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "meeting": {
                      "$ref": "#/components/schemas/NotepadMeeting"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "422": {
            "$ref": "#/components/responses/NotepadError"
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}/images": {
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Upload an image for the note body",
        "description": "Quill inlines a pasted or chosen image as a base64 data URI, which pushes\na note past the body length cap and stops it saving at all. This uploads\ninstead and returns a URL to embed.\n\nThe URL is permanent, absolute and bearer-readable by design: it is\nwritten INTO the stored note body, an `<img>` tag cannot send an\nAuthorization header, and the same body is rendered by both the web app\nand the desktop client (whose renderer origin is not the tenant host, so\na path-only URL would never load there). `url` may be null if the URL\ncould not be built — nothing is embedded in that case.\n\nThe file is stored as a platform `MediaItem` subject-scoped to the\nmeeting, so type and size are enforced by the shared upload gate.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The uploaded image",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "image": {
                      "type": "object",
                      "properties": {
                        "url": {
                          "type": "string",
                          "nullable": true,
                          "description": "Absolute, permanent, embeddable. Null when the URL could not be built."
                        },
                        "byte_size": {
                          "type": "integer",
                          "nullable": true
                        },
                        "content_type": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Caller cannot edit this note",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          },
          "422": {
            "description": "`file_required` or `invalid_image`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}/section": {
      "patch": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Replace one AI section's content",
        "description": "The section editor. `section` is the SERVER vocabulary — note\n`insights`, not the client's `key_insights` tab name.\n\nResponds with the whole artifacts body so the client can re-render the\nsection without a refetch.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "section",
                  "content"
                ],
                "properties": {
                  "section": {
                    "$ref": "#/components/schemas/NotepadSectionName"
                  },
                  "content": {
                    "description": "A string for `summary`; a structured list for `action_items` / `insights`.",
                    "oneOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "additionalProperties": true
                        }
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The saved section, with refreshed artifacts",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadSectionSaveResponse"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "422": {
            "$ref": "#/components/responses/NotepadError"
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}/section/revise": {
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "AI-revise one section",
        "description": "Rewrites, expands, refines or simplifies the section's current content and saves the result. Responds with the same body as a section save.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "section",
                  "action"
                ],
                "properties": {
                  "section": {
                    "$ref": "#/components/schemas/NotepadSectionName"
                  },
                  "action": {
                    "type": "string",
                    "enum": [
                      "rewrite",
                      "expand",
                      "refine",
                      "simplify"
                    ]
                  },
                  "instruction": {
                    "type": "string",
                    "description": "Optional free-text steer for the revision."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The revised section",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadSectionSaveResponse"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "422": {
            "$ref": "#/components/responses/NotepadError"
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}/section/versions": {
      "get": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Edit history for one AI section",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          },
          {
            "name": "section",
            "in": "query",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/NotepadSectionName"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Versions, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "meeting_id": {
                      "type": "integer"
                    },
                    "section": {
                      "$ref": "#/components/schemas/NotepadSectionName"
                    },
                    "versions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NotepadSectionVersion"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "422": {
            "$ref": "#/components/responses/NotepadError"
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}/section/restore": {
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Roll a section back to an earlier version",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "section",
                  "version_id"
                ],
                "properties": {
                  "section": {
                    "$ref": "#/components/schemas/NotepadSectionName"
                  },
                  "version_id": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The restored section",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadSectionSaveResponse"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "422": {
            "$ref": "#/components/responses/NotepadError"
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}/action_items": {
      "get": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "A meeting's action items",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Defaults to the maximum, which is what shipped clients get.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Action items with assignee identity and due labels",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "meeting_id": {
                      "type": "integer"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "processing",
                        "completed",
                        "failed"
                      ],
                      "description": "The MEETING vocabulary. Ambiguous by name across this meeting's three reads — see `meeting_status`."
                    },
                    "meeting_status": {
                      "type": "string",
                      "enum": [
                        "processing",
                        "completed",
                        "failed"
                      ],
                      "description": "The meeting's own lifecycle, unambiguously named."
                    },
                    "artifacts_status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "ready",
                        "failed"
                      ],
                      "description": "Whether the AI output exists yet."
                    },
                    "action_items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NotepadActionItem"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Counts are computed IN THE DB, never off the returned page, so a capped page never makes the tab's totals lie.",
                      "properties": {
                        "open_count": {
                          "type": "integer"
                        },
                        "total_count": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "has_more": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          }
        }
      },
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Add an action item to a meeting",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "title"
                ],
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "assignee_user_id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "due_date": {
                    "type": "string",
                    "format": "date",
                    "nullable": true
                  },
                  "commitment_type": {
                    "type": "string",
                    "enum": [
                      "hard",
                      "soft"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created action item",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "action_item": {
                      "$ref": "#/components/schemas/NotepadActionItem"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "422": {
            "$ref": "#/components/responses/NotepadValidationError"
          }
        }
      }
    },
    "/ai_notepad/action_items": {
      "get": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Action items assigned to the caller, across meetings",
        "description": "The \"My Action Items\" view. Each row carries a lightweight meeting reference so the client can deep-link back to its source.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "assignee",
            "in": "query",
            "description": "Only `me` is supported.",
            "schema": {
              "type": "string",
              "enum": [
                "me"
              ],
              "default": "me"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "completed"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated cross-meeting action items",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "action_items": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/NotepadActionItem"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "meeting": {
                                "$ref": "#/components/schemas/NotepadMeetingRef"
                              }
                            }
                          }
                        ]
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/NotepadCursorMeta"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/NotepadForbidden"
          }
        }
      }
    },
    "/ai_notepad/action_items/{id}": {
      "patch": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Update an action item",
        "description": "Edits the title, assignee, due date or commitment type, and toggles `done` (which maps onto the server's pending/completed status). An assignee change emits the realtime `action_item:assigned` event.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "done": {
                    "type": "boolean"
                  },
                  "assignee_user_id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "due_date": {
                    "type": "string",
                    "format": "date",
                    "nullable": true
                  },
                  "commitment_type": {
                    "type": "string",
                    "enum": [
                      "hard",
                      "soft"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated action item",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "action_item": {
                      "$ref": "#/components/schemas/NotepadActionItem"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "422": {
            "$ref": "#/components/responses/NotepadValidationError"
          }
        }
      },
      "put": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Update an action item (PUT alias)",
        "description": "Alias of PATCH on this path — Rails' `resources` answers both verbs identically. Documented because it is callable, not because it is preferred; new clients should use PATCH.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "done": {
                    "type": "boolean"
                  },
                  "assignee_user_id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "due_date": {
                    "type": "string",
                    "format": "date",
                    "nullable": true
                  },
                  "commitment_type": {
                    "type": "string",
                    "enum": [
                      "hard",
                      "soft"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated action item",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "action_item": {
                      "$ref": "#/components/schemas/NotepadActionItem"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "422": {
            "$ref": "#/components/responses/NotepadValidationError"
          }
        }
      },
      "delete": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Delete an action item",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "deleted": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          }
        }
      }
    },
    "/ai_notepad/meetings/{meeting_id}/members": {
      "get": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Who a meeting is shared with",
        "description": "The creator appears as an immutable synthetic `owner` member. People search for the add flow reuses `GET /api/v1/chat/users/search`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingIdPath"
          }
        ],
        "responses": {
          "200": {
            "description": "Members",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "members": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NotepadMember"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          }
        }
      },
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Share a meeting with a teammate",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingIdPath"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "user_id"
                ],
                "properties": {
                  "user_id": {
                    "type": "integer"
                  },
                  "role": {
                    "type": "string",
                    "enum": [
                      "viewer",
                      "editor"
                    ],
                    "default": "viewer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The added member",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "member": {
                      "$ref": "#/components/schemas/NotepadMember"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/NotepadForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "422": {
            "$ref": "#/components/responses/NotepadValidationError"
          }
        }
      }
    },
    "/ai_notepad/meetings/{meeting_id}/members/{id}": {
      "patch": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Change a member's role",
        "description": "`id` is the member's USER id. The owner's role is immutable.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingIdPath"
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The member's user id.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "role"
                ],
                "properties": {
                  "role": {
                    "type": "string",
                    "enum": [
                      "viewer",
                      "editor"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated member",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "member": {
                      "$ref": "#/components/schemas/NotepadMember"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/NotepadForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "422": {
            "$ref": "#/components/responses/NotepadValidationError"
          }
        }
      },
      "put": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Change a member's role (PUT alias)",
        "description": "Alias of PATCH on this path — Rails' `resources` answers both verbs identically. Documented because it is callable, not because it is preferred; new clients should use PATCH.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingIdPath"
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "role"
                ],
                "properties": {
                  "role": {
                    "type": "string",
                    "enum": [
                      "viewer",
                      "editor"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated member",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "member": {
                      "$ref": "#/components/schemas/NotepadMember"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "422": {
            "$ref": "#/components/responses/NotepadValidationError"
          }
        }
      },
      "delete": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Remove a member's access",
        "description": "`id` is the member's USER id. The owner cannot be removed.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingIdPath"
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The member's user id.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Removed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user_id": {
                      "type": "integer"
                    },
                    "removed": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/NotepadForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}/audio_overview": {
      "get": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "The narrated audio overview",
        "description": "`available: false` is the normal not-generated-yet answer. When available, `url` is signed and expiring — refetch rather than storing it.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "responses": {
          "200": {
            "description": "Audio overview state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadAudioOverviewDetail"
                }
              }
            }
          },
          "403": {
            "description": "The audio-overview feature is off for this business",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "502": {
            "description": "The overview exists but storage could not mint a URL for it (`audio_overview_url_unavailable`). A named, retryable failure rather than a 200 carrying a `url` the player will choke on.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Generate the audio overview",
        "description": "Returns 202; the client awaits the realtime `audio_overview:ready` event, then GETs the signed URL. Requires editor-level membership.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "responses": {
          "202": {
            "description": "Generation enqueued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "meeting_id": {
                      "type": "integer"
                    },
                    "status": {
                      "type": "string",
                      "description": "Always `generating`. This is the GENERATION's state, not the meeting's — see `meeting_status` for that.",
                      "enum": [
                        "generating"
                      ]
                    },
                    "state": {
                      "type": "string",
                      "description": "The SAME key the two reads carry, so a client that fires this POST has something to poll on. `generating` here; `ready` / `failed` / `none` on a read.",
                      "enum": [
                        "generating"
                      ]
                    },
                    "processing": {
                      "type": "object",
                      "description": "Whether THIS request started anything. A generation already in flight for this meeting holds the concurrency key, so the enqueue aborts and nothing new was started — the overview is still being generated, just not by this call.",
                      "properties": {
                        "started": {
                          "type": "boolean"
                        },
                        "reason": {
                          "type": "string",
                          "enum": [
                            "queued",
                            "already_generating"
                          ]
                        }
                      }
                    },
                    "meeting_status": {
                      "type": "string",
                      "enum": [
                        "processing",
                        "completed",
                        "failed"
                      ],
                      "description": "The meeting's own lifecycle, unambiguously named."
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Feature off, or caller lacks editor access",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "422": {
            "$ref": "#/components/responses/NotepadError"
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}/chat_history": {
      "get": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "The persisted AI-chat conversation for a meeting",
        "description": "Lets a client re-hydrate the chat tab on reopen instead of starting blank. The streamed POST persists both turns.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "responses": {
          "200": {
            "description": "Chat messages, chronological",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "meeting_id": {
                      "type": "integer"
                    },
                    "messages": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NotepadChatMessage"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}/chat": {
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Ask a question about one meeting (streamed)",
        "description": "**Server-Sent Events, not JSON.** The response is `text/event-stream`\ncarrying incremental `delta` frames, a terminal `complete` frame (message\nid, model, usage) and an `error` frame if generation fails mid-stream.\nAnswers are grounded in the meeting and carry citations back to\ntranscript segments.\n\nThe answer is generated server-side with the server-held model key — no\nmodel credential ever reaches a client. A short-lived `chat_stream`\ncapture token from `POST /captures/token` is re-checked when presented.\n\nBoth turns are persisted; read them back with `chat_history`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "message"
                ],
                "properties": {
                  "message": {
                    "type": "string"
                  },
                  "capture_token": {
                    "type": "string",
                    "description": "Optional scoped token from `POST /captures/token`."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "An SSE stream of delta / complete / error frames",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string",
                  "description": "Newline-delimited SSE frames. Not JSON — do not parse the body as a document."
                }
              }
            }
          },
          "403": {
            "description": "Meeting chat is off for this business, or access denied",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          }
        }
      }
    },
    "/ai_notepad/chat": {
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Ask a question across your notes (streamed)",
        "description": "**Server-Sent Events, not JSON** — same transport as the per-meeting\nchat. `source_scope` chooses which of the caller's visible meetings\nground the answer.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "message"
                ],
                "properties": {
                  "message": {
                    "type": "string"
                  },
                  "source_scope": {
                    "type": "string",
                    "enum": [
                      "all",
                      "notebook",
                      "selected",
                      "date_range"
                    ],
                    "default": "all"
                  },
                  "notebook_id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "meeting_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    }
                  },
                  "from": {
                    "type": "string",
                    "format": "date"
                  },
                  "to": {
                    "type": "string",
                    "format": "date"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "An SSE stream of delta / complete / error frames",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/NotepadForbidden"
          }
        }
      }
    },
    "/ai_notepad/captures/token": {
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Mint a short-lived scoped capture token",
        "description": "A stateless, signed grant bound to ONE meeting and one scope, with a\n5-minute ceiling. It exists so a client never holds a model API key:\nit presents this to the streaming surfaces instead. Refresh by calling\nagain.\n\nOnly `chat_stream` is issuable today; an unknown scope is a 422 rather\nthan a silently-issued token.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "scope",
                  "meeting_id"
                ],
                "properties": {
                  "scope": {
                    "type": "string",
                    "enum": [
                      "chat_stream"
                    ]
                  },
                  "meeting_id": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The minted token",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "capture_token": {
                      "type": "object",
                      "properties": {
                        "token": {
                          "type": "string"
                        },
                        "scope": {
                          "type": "string",
                          "enum": [
                            "chat_stream"
                          ]
                        },
                        "meeting_id": {
                          "type": "integer"
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "expires_in": {
                          "type": "integer",
                          "description": "Seconds. At most 300."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "422": {
            "description": "Unknown or unsupported scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}/send_to_chat": {
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Post a meeting's sections into a chat room",
        "description": "Composes the chosen sections as Markdown and posts them into a Shifts\nchat room the caller belongs to, through the same path the chat surface\nuses.\n\nThe ONE endpoint in this namespace that requires a token scope —\n`write:chat` — because it writes a chat message. Also re-checks that the\nChat app is enabled and visible to the caller.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "room_id",
                  "sections"
                ],
                "properties": {
                  "room_id": {
                    "type": "integer"
                  },
                  "sections": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "summary",
                        "key_insights",
                        "action_items"
                      ]
                    }
                  },
                  "client_uuid": {
                    "type": "string",
                    "description": "Client-side dedup key for the posted message."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Posted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "room_id": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`forbidden` (missing write:chat), `app_not_enabled`,\n`app_not_accessible`, or `im_not_allowed`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          },
          "404": {
            "description": "`room_not_found`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/NotepadError"
          }
        }
      }
    },
    "/ai_notepad/notebooks": {
      "get": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Notebooks the caller can file notes into",
        "description": "Every row is a `notebook_id` both write paths accept — created by the caller, or shared with them as editor/owner.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Notebooks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "notebooks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NotepadNotebook"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/NotepadForbidden"
          }
        }
      },
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Create a notebook",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "title"
                ],
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created notebook",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "notebook": {
                      "$ref": "#/components/schemas/NotepadNotebook"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/NotepadValidationError"
          }
        }
      }
    },
    "/ai_notepad/notebooks/{id}": {
      "patch": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Rename or re-describe a notebook",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated notebook",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "notebook": {
                      "$ref": "#/components/schemas/NotepadNotebook"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "422": {
            "$ref": "#/components/responses/NotepadValidationError"
          }
        }
      },
      "put": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Rename or re-describe a notebook (PUT alias)",
        "description": "Alias of PATCH on this path — Rails' `resources` answers both verbs identically. Documented because it is callable, not because it is preferred; new clients should use PATCH.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated notebook",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "notebook": {
                      "$ref": "#/components/schemas/NotepadNotebook"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "422": {
            "$ref": "#/components/responses/NotepadValidationError"
          }
        }
      },
      "delete": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Delete a notebook",
        "description": "The notes inside are not deleted — they are unfiled.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "deleted": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "422": {
            "$ref": "#/components/responses/NotepadError"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "API token authentication"
      },
      "ServiceAccountAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Secret",
        "description": "Service account secret (used with Bearer token)"
      },
      "InternalAPI": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Internal API authentication with X-Internal-API header"
      },
      "ChangelogSecret": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Changelog-Secret",
        "description": "Changelog-specific secret for internal operations"
      }
    },
    "parameters": {
      "Page": {
        "name": "page",
        "in": "query",
        "description": "Page number for pagination",
        "schema": {
          "type": "integer",
          "default": 1,
          "minimum": 1
        }
      },
      "PerPage": {
        "name": "per_page",
        "description": "Number of items per page",
        "in": "query",
        "schema": {
          "type": "integer",
          "default": 25,
          "minimum": 1,
          "maximum": 100
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Bad request",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Unauthorized",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "UnauthorizedError": {
        "description": "Authentication required or token invalid",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Forbidden",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ForbiddenError": {
        "description": "Insufficient permissions to access this resource",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Not found",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFoundError": {
        "description": "Resource not found",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ValidationError": {
        "description": "Validation error",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ValidationErrors"
            }
          }
        }
      },
      "InternalServerError": {
        "description": "Internal server error",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests - rate limit exceeded",
        "headers": {
          "Retry-After": {
            "description": "Seconds until rate limit reset",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Limit": {
            "description": "Total requests allowed per window",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests remaining in current window",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "BusinessDiscoveryResult": {
        "type": "object",
        "description": "Tenants the submitted email address can sign in to. Always returned with\na 200, including when the address is unknown or has no active tenants —\nin both of those cases `businesses` is an empty array and `total_count`\nis 0. The endpoint is unauthenticated, so it deliberately does NOT\ndistinguish \"no such account\" from \"account with no active tenants\":\ndoing so would make it an account-existence oracle.\n",
        "properties": {
          "businesses": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 456
                },
                "name": {
                  "type": "string",
                  "example": "Office Chat Solutions"
                },
                "subdomain": {
                  "type": "string",
                  "example": "officechat"
                },
                "logo_url": {
                  "type": "string",
                  "nullable": true,
                  "description": "Absolute logo URL, or null when the tenant has no logo attached.\nServed from the tenant's OWN host (matching api_base_url), not\nthe host that answered this request.\n",
                  "example": "https://acme.workforce.mangoapps.com/rails/active_storage/blobs/redirect/…/logo.png"
                },
                "api_base_url": {
                  "type": "string",
                  "description": "Origin the client should send the subsequent login request to.\nAlready carries the environment suffix (-dev / -qa / -staging;\nnone in production), so clients must not rebuild the host from\n`subdomain` themselves.\n",
                  "example": "https://officechat.workforce.mangoapps.com"
                }
              }
            }
          },
          "total_count": {
            "type": "integer",
            "example": 2
          }
        }
      },
      "SupportTicketSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "status_color_class": {
            "type": "string",
            "description": "Bootstrap badge class mapped from status",
            "example": "bg-warning"
          },
          "priority": {
            "type": "string"
          },
          "priority_color_class": {
            "type": "string",
            "description": "Bootstrap badge class mapped from priority",
            "example": "bg-danger"
          },
          "escalated": {
            "type": "boolean"
          },
          "request_type": {
            "type": "string"
          },
          "requester": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string"
              }
            }
          },
          "assignee": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string"
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "is_overdue": {
            "type": "boolean",
            "nullable": true
          },
          "sla_breached": {
            "type": "boolean",
            "description": "True when the SLA deadline (sla_due_at) has passed and the ticket is still open"
          },
          "sla_due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "SLA deadline computed from priority and assignment time"
          }
        }
      },
      "SupportTicketDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SupportTicketSummary"
          },
          {
            "type": "object",
            "properties": {
              "description": {
                "type": "string"
              },
              "resolution_notes": {
                "type": "string",
                "nullable": true
              },
              "resolved_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "closed_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "comments_count": {
                "type": "integer"
              },
              "comments": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TicketComment"
                }
              }
            }
          }
        ]
      },
      "TicketComment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "content": {
            "type": "string"
          },
          "author": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "internal": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ServiceCatalogType": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "description": "Slug identifier for the request type"
          },
          "title": {
            "type": "string"
          },
          "icon": {
            "type": "string",
            "description": "Font Awesome icon class"
          },
          "color": {
            "type": "string",
            "description": "Bootstrap color name"
          },
          "description": {
            "type": "string"
          },
          "examples": {
            "type": "string"
          },
          "estimated_resolution": {
            "type": "string"
          },
          "sla_hours": {
            "type": "integer"
          },
          "custom": {
            "type": "boolean"
          },
          "category": {
            "type": "string"
          },
          "priority": {
            "type": "string"
          },
          "service_department_id": {
            "type": "integer",
            "nullable": true
          }
        }
      },
      "KbSearchResult": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "snippet": {
            "type": "string",
            "description": "Content excerpt (up to 500 characters)"
          },
          "url": {
            "type": "string",
            "description": "Absolute URL to the article"
          },
          "category": {
            "type": "string",
            "nullable": true
          },
          "relevance_score": {
            "type": "number",
            "format": "float",
            "description": "Normalized relevance in [0, 1]"
          },
          "source": {
            "type": "string",
            "description": "Where the result came from",
            "example": "help_article"
          }
        }
      },
      "AskAiMessageResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          },
          "conversation_id": {
            "type": "string",
            "format": "uuid",
            "description": "Unique identifier for this conversation",
            "example": "550e8400-e29b-41d4-a716-446655440000"
          },
          "status": {
            "type": "string",
            "enum": [
              "processing",
              "completed",
              "error"
            ],
            "example": "processing"
          },
          "websocket": {
            "type": "object",
            "description": "WebSocket subscription details",
            "properties": {
              "channel": {
                "type": "string",
                "example": "AiResponseChannel"
              },
              "subscription": {
                "type": "object",
                "properties": {
                  "conversation_id": {
                    "type": "string",
                    "format": "uuid"
                  }
                }
              }
            }
          }
        }
      },
      "AskAiConversation": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Conversation session ID"
          },
          "type": {
            "type": "string",
            "enum": [
              "forms",
              "tasks",
              "epms",
              "general"
            ],
            "example": "general"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "completed",
              "archived"
            ],
            "example": "active"
          },
          "message_count": {
            "type": "integer",
            "example": 12
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "AskAiMessage": {
        "type": "object",
        "properties": {
          "role": {
            "type": "string",
            "enum": [
              "user",
              "assistant",
              "system"
            ],
            "example": "assistant"
          },
          "content": {
            "type": "string",
            "example": "You have 15 PTO days remaining for this year."
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true,
            "description": "Additional message metadata (intent, tool usage, etc.)"
          }
        }
      },
      "AskAiAgentInfo": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "example": "Scheduling Assistant"
          },
          "slug": {
            "type": "string",
            "example": "scheduling"
          },
          "description": {
            "type": "string",
            "example": "Helps with shifts, schedules, and time off requests"
          },
          "icon": {
            "type": "string",
            "example": "calendar"
          },
          "examples": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "What shifts do I have this week?",
              "Request time off for next Friday",
              "Who is working tomorrow?"
            ]
          }
        }
      },
      "VoiceTokenResponse": {
        "type": "object",
        "description": "Response from voice_token endpoint with all connection details for voice mode",
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          },
          "data": {
            "type": "object",
            "properties": {
              "token": {
                "type": "string",
                "description": "Ephemeral token for OpenAI Realtime API (expires in 60 seconds)",
                "example": "eph_abc123..."
              },
              "session_id": {
                "type": "string",
                "format": "uuid",
                "description": "Unique session ID for this voice session",
                "example": "550e8400-e29b-41d4-a716-446655440000"
              },
              "expires_at": {
                "type": "string",
                "format": "date-time",
                "description": "When the ephemeral token expires",
                "example": "2025-01-26T14:21:00Z"
              },
              "session_config": {
                "type": "object",
                "description": "Configuration for OpenAI Realtime session",
                "properties": {
                  "voice": {
                    "type": "string",
                    "example": "alloy"
                  },
                  "modalities": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "example": [
                      "text",
                      "audio"
                    ]
                  },
                  "turn_detection": {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "example": "server_vad"
                      },
                      "threshold": {
                        "type": "number",
                        "example": 0.5
                      }
                    }
                  }
                }
              },
              "limit_info": {
                "type": "object",
                "description": "Voice usage limits and current usage",
                "properties": {
                  "daily_used": {
                    "type": "integer",
                    "description": "Minutes used today",
                    "example": 5
                  },
                  "daily_limit": {
                    "type": "integer",
                    "description": "Daily limit in minutes",
                    "example": 30
                  },
                  "remaining": {
                    "type": "integer",
                    "description": "Remaining minutes for today",
                    "example": 25
                  },
                  "session_limit": {
                    "type": "integer",
                    "description": "Max minutes per session",
                    "example": 15
                  },
                  "monthly_sessions": {
                    "type": "integer",
                    "description": "Total voice sessions this billing period"
                  },
                  "monthly_minutes": {
                    "type": "number",
                    "description": "Total minutes used this billing period"
                  },
                  "monthly_cost_cents": {
                    "type": "integer",
                    "description": "Total cost in cents this billing period"
                  }
                }
              },
              "billing_info": {
                "type": "object",
                "description": "Billing information for voice usage",
                "properties": {
                  "balance": {
                    "type": "integer",
                    "description": "Available balance in cents",
                    "example": 1000
                  },
                  "cost_per_minute_cents": {
                    "type": "integer",
                    "description": "Cost per minute in cents",
                    "example": 10
                  }
                }
              },
              "connections": {
                "type": "object",
                "description": "Connection details for WebSocket and OpenAI",
                "properties": {
                  "actioncable": {
                    "type": "object",
                    "description": "ActionCable WebSocket connection details",
                    "properties": {
                      "url": {
                        "type": "string",
                        "description": "WebSocket URL for ActionCable",
                        "example": "wss://mycompany.workforce.mangoapps.com/cable"
                      },
                      "channel": {
                        "type": "string",
                        "description": "Channel name to subscribe to",
                        "example": "VoiceRealtimeChannel"
                      },
                      "params": {
                        "type": "object",
                        "description": "Parameters to include in subscription",
                        "properties": {
                          "session_id": {
                            "type": "string",
                            "format": "uuid"
                          }
                        }
                      }
                    }
                  },
                  "openai_realtime": {
                    "type": "object",
                    "description": "OpenAI Realtime API connection details",
                    "properties": {
                      "url": {
                        "type": "string",
                        "description": "OpenAI Realtime API base URL",
                        "example": "https://api.openai.com/v1/realtime"
                      },
                      "model": {
                        "type": "string",
                        "description": "OpenAI model for voice",
                        "example": "gpt-4o-realtime-preview"
                      },
                      "sdp_endpoint": {
                        "type": "string",
                        "description": "Endpoint for SDP exchange",
                        "example": "https://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "VoiceSessionStatus": {
        "type": "object",
        "description": "Current status of a voice session",
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          },
          "session": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid",
                "description": "Session ID",
                "example": "550e8400-e29b-41d4-a716-446655440000"
              },
              "status": {
                "type": "string",
                "enum": [
                  "active",
                  "completed",
                  "pending"
                ],
                "description": "Current session status",
                "example": "active"
              },
              "started_at": {
                "type": "string",
                "format": "date-time",
                "description": "When the session started"
              },
              "duration_seconds": {
                "type": "integer",
                "description": "Current or total session duration in seconds",
                "example": 120
              },
              "queries_processed": {
                "type": "integer",
                "description": "Number of voice queries processed in this session",
                "example": 5
              }
            }
          }
        }
      },
      "VoiceSessionEndResponse": {
        "type": "object",
        "description": "Response when ending a voice session",
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          },
          "session": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid",
                "description": "Session ID that was ended",
                "example": "550e8400-e29b-41d4-a716-446655440000"
              },
              "duration_minutes": {
                "type": "number",
                "description": "Total session duration in minutes",
                "example": 5
              },
              "cost_cents": {
                "type": "integer",
                "description": "Total cost of the session in cents",
                "example": 50
              },
              "billed": {
                "type": "boolean",
                "description": "Whether billing has been processed",
                "example": true
              }
            }
          }
        }
      },
      "TinyTakeCapture": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "cap_abc123"
          },
          "name": {
            "type": "string",
            "example": "Meeting Notes 2024-01-15"
          },
          "type": {
            "type": "string",
            "enum": [
              "image",
              "video"
            ],
            "example": "image"
          },
          "mime_type": {
            "type": "string",
            "example": "image/png"
          },
          "size_bytes": {
            "type": "integer",
            "format": "int64",
            "example": 245760
          },
          "thumbnail_url": {
            "type": "string",
            "format": "uri",
            "nullable": true
          },
          "visibility": {
            "type": "string",
            "enum": [
              "private",
              "shared",
              "public"
            ],
            "example": "private"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "meeting",
              "notes"
            ]
          },
          "folder_id": {
            "type": "string",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "TinyTakeCaptureDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/TinyTakeCapture"
          },
          {
            "type": "object",
            "properties": {
              "width": {
                "type": "integer",
                "nullable": true,
                "example": 1920
              },
              "height": {
                "type": "integer",
                "nullable": true,
                "example": 1080
              },
              "duration_seconds": {
                "type": "number",
                "nullable": true,
                "description": "Video duration (null for images)",
                "example": 120.5
              },
              "download_url": {
                "type": "string",
                "format": "uri"
              },
              "share_url": {
                "type": "string",
                "format": "uri",
                "nullable": true
              },
              "share_expires_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "has_annotations": {
                "type": "boolean",
                "example": false
              }
            }
          }
        ]
      },
      "TinyTakeFolder": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "fld_abc123"
          },
          "name": {
            "type": "string",
            "example": "Project Screenshots"
          },
          "parent_id": {
            "type": "string",
            "nullable": true
          },
          "capture_count": {
            "type": "integer",
            "example": 12
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "TinyTakeStorageInfo": {
        "type": "object",
        "properties": {
          "used_bytes": {
            "type": "integer",
            "format": "int64",
            "example": 524288000
          },
          "total_bytes": {
            "type": "integer",
            "format": "int64",
            "example": 5368709120
          },
          "used_percentage": {
            "type": "number",
            "format": "float",
            "example": 9.77
          },
          "capture_count": {
            "type": "integer",
            "example": 156
          },
          "video_count": {
            "type": "integer",
            "example": 23
          },
          "image_count": {
            "type": "integer",
            "example": 133
          },
          "limits": {
            "type": "object",
            "properties": {
              "max_video_duration_seconds": {
                "type": "integer",
                "nullable": true,
                "description": "Max video recording duration (null for unlimited)",
                "example": 3600
              },
              "max_file_size_bytes": {
                "type": "integer",
                "format": "int64",
                "example": 2147483648
              },
              "annotations_enabled": {
                "type": "boolean",
                "example": true
              }
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "near_limit",
              "at_limit"
            ],
            "example": "active"
          }
        }
      },
      "TinyTakeSettings": {
        "type": "object",
        "properties": {
          "hotkeys": {
            "type": "object",
            "properties": {
              "capture_screen": {
                "type": "string",
                "example": "Ctrl+Shift+S"
              },
              "capture_region": {
                "type": "string",
                "example": "Ctrl+Shift+R"
              },
              "capture_window": {
                "type": "string",
                "example": "Ctrl+Shift+W"
              },
              "start_recording": {
                "type": "string",
                "example": "Ctrl+Shift+V"
              },
              "stop_recording": {
                "type": "string",
                "example": "Ctrl+Shift+X"
              },
              "pause_recording": {
                "type": "string",
                "example": "Ctrl+Shift+P"
              }
            }
          },
          "defaults": {
            "type": "object",
            "properties": {
              "image_format": {
                "type": "string",
                "enum": [
                  "png",
                  "jpg",
                  "gif"
                ],
                "example": "png"
              },
              "video_format": {
                "type": "string",
                "enum": [
                  "mp4",
                  "webm"
                ],
                "example": "mp4"
              },
              "video_quality": {
                "type": "string",
                "enum": [
                  "low",
                  "medium",
                  "high",
                  "lossless"
                ],
                "example": "high"
              },
              "video_fps": {
                "type": "integer",
                "enum": [
                  15,
                  30,
                  60
                ],
                "example": 30
              },
              "audio_enabled": {
                "type": "boolean",
                "example": true
              },
              "microphone_enabled": {
                "type": "boolean",
                "example": false
              },
              "auto_upload": {
                "type": "boolean",
                "example": true
              },
              "copy_link_after_upload": {
                "type": "boolean",
                "example": true
              },
              "show_cursor": {
                "type": "boolean",
                "example": true
              },
              "highlight_clicks": {
                "type": "boolean",
                "example": false
              }
            }
          }
        }
      },
      "TinyTakeSettingsUpdate": {
        "type": "object",
        "properties": {
          "hotkeys": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "defaults": {
            "type": "object",
            "properties": {
              "image_format": {
                "type": "string",
                "enum": [
                  "png",
                  "jpg",
                  "gif"
                ]
              },
              "video_format": {
                "type": "string",
                "enum": [
                  "mp4",
                  "webm"
                ]
              },
              "video_quality": {
                "type": "string",
                "enum": [
                  "low",
                  "medium",
                  "high",
                  "lossless"
                ]
              },
              "video_fps": {
                "type": "integer",
                "enum": [
                  15,
                  30,
                  60
                ]
              },
              "audio_enabled": {
                "type": "boolean"
              },
              "microphone_enabled": {
                "type": "boolean"
              },
              "auto_upload": {
                "type": "boolean"
              },
              "copy_link_after_upload": {
                "type": "boolean"
              },
              "show_cursor": {
                "type": "boolean"
              },
              "highlight_clicks": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "TinyTakeTag": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "example": "meeting"
          },
          "count": {
            "type": "integer",
            "description": "Number of captures with this tag",
            "example": 42
          },
          "last_used_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "TinyTakeAnnotation": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "arrow",
              "line",
              "rectangle",
              "ellipse",
              "text",
              "highlight",
              "blur",
              "freehand",
              "callout"
            ]
          },
          "position": {
            "type": "object",
            "properties": {
              "x": {
                "type": "number"
              },
              "y": {
                "type": "number"
              },
              "width": {
                "type": "number"
              },
              "height": {
                "type": "number"
              },
              "points": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "x": {
                      "type": "number"
                    },
                    "y": {
                      "type": "number"
                    }
                  }
                },
                "description": "For freehand/line annotations"
              }
            }
          },
          "style": {
            "type": "object",
            "properties": {
              "stroke_color": {
                "type": "string",
                "example": "#FF0000"
              },
              "fill_color": {
                "type": "string",
                "nullable": true
              },
              "stroke_width": {
                "type": "integer"
              },
              "opacity": {
                "type": "number",
                "minimum": 0,
                "maximum": 1
              },
              "font_size": {
                "type": "integer"
              },
              "font_family": {
                "type": "string"
              }
            }
          },
          "text": {
            "type": "string",
            "nullable": true,
            "description": "Text content for text/callout annotations"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "TinyTakeAnnotationInput": {
        "type": "object",
        "required": [
          "type",
          "position"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Optional ID to preserve existing annotation"
          },
          "type": {
            "type": "string",
            "enum": [
              "arrow",
              "line",
              "rectangle",
              "ellipse",
              "text",
              "highlight",
              "blur",
              "freehand",
              "callout"
            ]
          },
          "position": {
            "type": "object",
            "required": [
              "x",
              "y"
            ],
            "properties": {
              "x": {
                "type": "number"
              },
              "y": {
                "type": "number"
              },
              "width": {
                "type": "number"
              },
              "height": {
                "type": "number"
              },
              "points": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "x": {
                      "type": "number"
                    },
                    "y": {
                      "type": "number"
                    }
                  }
                }
              }
            }
          },
          "style": {
            "type": "object",
            "properties": {
              "stroke_color": {
                "type": "string"
              },
              "fill_color": {
                "type": "string"
              },
              "stroke_width": {
                "type": "integer"
              },
              "opacity": {
                "type": "number"
              },
              "font_size": {
                "type": "integer"
              },
              "font_family": {
                "type": "string"
              }
            }
          },
          "text": {
            "type": "string"
          }
        }
      },
      "TinyTakeUser": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "avatar_url": {
            "type": "string",
            "format": "uri",
            "nullable": true
          }
        }
      },
      "TinyTakeShare": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "user",
              "team"
            ]
          },
          "recipient": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/TinyTakeUser"
              },
              {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "member_count": {
                    "type": "integer"
                  }
                }
              }
            ]
          },
          "permission": {
            "type": "string",
            "enum": [
              "view",
              "comment",
              "edit"
            ]
          },
          "shared_at": {
            "type": "string",
            "format": "date-time"
          },
          "shared_by": {
            "$ref": "#/components/schemas/TinyTakeUser"
          }
        }
      },
      "TinyTakeComment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "content": {
            "type": "string"
          },
          "author": {
            "$ref": "#/components/schemas/TinyTakeUser"
          },
          "parent_id": {
            "type": "string",
            "nullable": true
          },
          "position": {
            "type": "object",
            "nullable": true,
            "properties": {
              "x": {
                "type": "number"
              },
              "y": {
                "type": "number"
              },
              "timestamp": {
                "type": "number"
              }
            }
          },
          "reply_count": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "is_edited": {
            "type": "boolean"
          }
        }
      },
      "TinyTakeVideoChapter": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "timestamp": {
            "type": "number",
            "description": "Start time in seconds"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "thumbnail_url": {
            "type": "string",
            "format": "uri",
            "nullable": true
          }
        }
      },
      "TinyTakeActivity": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "action": {
            "type": "string",
            "enum": [
              "upload",
              "view",
              "download",
              "share",
              "delete",
              "edit",
              "comment",
              "annotate"
            ]
          },
          "capture": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "thumbnail_url": {
                "type": "string",
                "format": "uri",
                "nullable": true
              }
            }
          },
          "actor": {
            "$ref": "#/components/schemas/TinyTakeUser"
          },
          "details": {
            "type": "object",
            "additionalProperties": true,
            "description": "Action-specific details",
            "example": {
              "shared_with": [
                "user@example.com"
              ]
            }
          },
          "ip_address": {
            "type": "string",
            "nullable": true
          },
          "user_agent": {
            "type": "string",
            "nullable": true
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "MessagingPaginationMeta": {
        "type": "object",
        "properties": {
          "total_count": {
            "type": "integer"
          },
          "total_pages": {
            "type": "integer"
          },
          "current_page": {
            "type": "integer"
          },
          "per_page": {
            "type": "integer"
          },
          "has_next_page": {
            "type": "boolean"
          },
          "has_prev_page": {
            "type": "boolean"
          }
        }
      },
      "MessagingParticipant": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "avatar_url": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "MessagingRecipient": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "text": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "meta": {
            "type": "string",
            "description": "Context line (title · employee id · email)."
          }
        }
      },
      "MessagingAttachment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "filename": {
            "type": "string"
          },
          "content_type": {
            "type": "string"
          },
          "byte_size": {
            "type": "integer"
          },
          "url": {
            "type": "string",
            "nullable": true,
            "description": "Absolute download URL."
          },
          "preview_url": {
            "type": "string",
            "nullable": true,
            "description": "Inline URL for raster images only."
          },
          "thumbnail_url": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "MessagingMessage": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "thread_id": {
            "type": "integer"
          },
          "author_id": {
            "type": "integer"
          },
          "author": {
            "$ref": "#/components/schemas/MessagingParticipant"
          },
          "body": {
            "type": "string"
          },
          "mentioned_user_ids": {
            "type": "array",
            "items": {
              "type": "integer"
            }
          },
          "edited_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "deleted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "attachments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MessagingAttachment"
            }
          },
          "voicenotes": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "audio_url": {
                  "type": "string",
                  "nullable": true
                },
                "transcript": {
                  "type": "string",
                  "nullable": true
                },
                "created_at": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            }
          }
        }
      },
      "MessagingThread": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "kind": {
            "type": "string",
            "enum": [
              "one_on_one",
              "group"
            ]
          },
          "title": {
            "type": "string"
          },
          "one_on_one": {
            "type": "boolean"
          },
          "archived_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "last_message_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "unread_count": {
            "type": "integer"
          },
          "muted": {
            "type": "boolean",
            "description": "Whether the CALLER has this conversation muted (their own participant `muted_until` is in the future). Present on both the list and detail responses so a client can render Mute vs Unmute.\n"
          },
          "last_message_preview": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "author_id": {
                "type": "integer"
              },
              "body": {
                "type": "string",
                "description": "Excerpt of the newest message, ready to render as-is. An attachment-only message has a blank body, so this carries \"Attachment\" / \"N attachments\" instead of an empty string.\n"
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "has_attachments": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "MessagingThreadDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/MessagingThread"
          },
          {
            "type": "object",
            "properties": {
              "participants": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/MessagingParticipant"
                }
              }
            }
          }
        ]
      },
      "ChatPaginationMeta": {
        "type": "object",
        "properties": {
          "current_page": {
            "type": "integer"
          },
          "per_page": {
            "type": "integer"
          },
          "total_count": {
            "type": "integer"
          },
          "total_pages": {
            "type": "integer"
          }
        }
      },
      "ChatUserSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "avatar_url": {
            "type": "string",
            "nullable": true
          },
          "avatar_updated_at": {
            "type": "integer",
            "nullable": true,
            "description": "Epoch seconds — cache-busting stamp."
          }
        }
      },
      "ChatRoom": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "room_type": {
            "type": "string",
            "enum": [
              "direct",
              "group",
              "channel"
            ]
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "topic": {
            "type": "string",
            "nullable": true
          },
          "last_message_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "archived_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "member_count": {
            "type": "integer"
          },
          "unread_count": {
            "type": "integer"
          },
          "last_read_message_id": {
            "type": "integer",
            "nullable": true
          },
          "pinned_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "is_muted": {
            "type": "boolean"
          },
          "mute_end_time": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "other_user_id": {
            "type": "integer",
            "nullable": true,
            "description": "DM peer (direct rooms only)."
          },
          "other_user_name": {
            "type": "string",
            "nullable": true
          },
          "other_user_avatar_url": {
            "type": "string",
            "nullable": true
          },
          "other_user_avatar_updated_at": {
            "type": "integer",
            "nullable": true
          },
          "last_message_preview": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "body": {
                "type": "string",
                "description": "Excerpt of the newest message, ready to render as-is. An attachment-only message has a blank body, so this carries a description of the attachment instead (\"Photo\", \"Video\", \"Attachment: report.pdf\", \"3 attachments\") — never an empty string. Author-less: prefix with `user_name` yourself.\n"
              },
              "user_id": {
                "type": "integer"
              },
              "user_name": {
                "type": "string"
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "has_attachments": {
                "type": "boolean"
              }
            }
          },
          "has_important_messages": {
            "type": "boolean"
          },
          "important_message_count": {
            "type": "integer"
          },
          "can_send_important_msg": {
            "type": "boolean"
          },
          "can_delete_self_message": {
            "type": "boolean"
          },
          "can_delete_other_message": {
            "type": "boolean"
          },
          "image_url": {
            "type": "string",
            "nullable": true,
            "description": "Group/channel photo."
          },
          "image_updated_at": {
            "type": "integer",
            "nullable": true
          }
        }
      },
      "ChatAttachment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "url": {
            "type": "string",
            "description": "Short-lived signed download URL."
          },
          "filename": {
            "type": "string"
          },
          "content_type": {
            "type": "string"
          },
          "byte_size": {
            "type": "integer"
          },
          "media_type": {
            "type": "string",
            "enum": [
              "image",
              "gif",
              "video",
              "file"
            ]
          },
          "processing_status": {
            "type": "string",
            "enum": [
              "pending",
              "ready",
              "failed"
            ]
          },
          "preview_url": {
            "type": "string",
            "nullable": true,
            "description": "Inline URL for raster images only."
          },
          "thumbnail_url": {
            "type": "string",
            "nullable": true
          },
          "hls_playlist_url": {
            "type": "string",
            "nullable": true,
            "description": "Video with CDN configured."
          },
          "subtitle_url": {
            "type": "string",
            "nullable": true,
            "description": "Video with subtitles ready."
          }
        }
      },
      "ChatReaction": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "emoji": {
            "type": "string"
          },
          "user_id": {
            "type": "integer"
          }
        }
      },
      "ChatMessage": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "room_id": {
            "type": "integer"
          },
          "user_id": {
            "type": "integer"
          },
          "body": {
            "type": "string",
            "description": "Message text, ALREADY MASKED where the caller is not entitled to it. When `body_masked` is true this is a fixed notice string, not the sender's words — render it verbatim and never treat it as content to cache, quote, search or preview. The mask is identical on every surface that hands this message to a recipient (this response, the room-list `last_message_preview`, the push notification, and the per-user `message:created` Pusher event on `private-user-inbox-<uid>-business-<bid>`), so a client that renders one and refetches the other never sees the text change."
          },
          "body_masked": {
            "type": "boolean",
            "description": "True when `body` is the read-receipt mask rather than the message text. Viewer-scoped: a read-receipt request stays masked for a recipient until they POST `/chat/rooms/{room_id}/messages/{id}/acknowledge`, which is what notifies the sender and returns the revealed body. Never true for the author or for a message the caller has already acknowledged."
          },
          "parent_message_id": {
            "type": "integer",
            "nullable": true
          },
          "parent": {
            "type": "object",
            "nullable": true,
            "description": "Quoted message excerpt (one-level inline quote).",
            "properties": {
              "id": {
                "type": "integer"
              },
              "body": {
                "type": "string",
                "description": "Excerpt (max 140 chars)."
              },
              "user_id": {
                "type": "integer"
              },
              "user_name": {
                "type": "string"
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "deleted": {
                "type": "boolean"
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "edited_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "deleted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "attachments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ChatAttachment"
            }
          },
          "reactions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ChatReaction"
            }
          },
          "client_uuid": {
            "type": "string",
            "nullable": true
          },
          "is_system": {
            "type": "boolean"
          },
          "ack_type": {
            "type": "string",
            "nullable": true,
            "enum": [
              "important",
              "read_receipt",
              null
            ]
          },
          "is_important": {
            "type": "boolean"
          },
          "is_read_receipt": {
            "type": "boolean"
          },
          "requires_ack": {
            "type": "boolean"
          },
          "is_acked": {
            "type": "boolean",
            "description": "Whether the CALLER has acknowledged."
          },
          "msg_ack_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "workflow_participants_count": {
            "type": "integer",
            "description": "Eligible recipients who acknowledged."
          },
          "ack_eligible_count": {
            "type": "integer",
            "description": "Total eligible recipients (excludes the author)."
          }
        }
      },
      "ChatMember": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Membership id (use for member update/remove)."
          },
          "user_id": {
            "type": "integer"
          },
          "role": {
            "type": "string",
            "enum": [
              "admin",
              "member"
            ]
          },
          "joined_at": {
            "type": "string",
            "format": "date-time"
          },
          "muted_until": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "last_read_message_id": {
            "type": "integer",
            "nullable": true
          },
          "user": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/ChatUserSummary"
              },
              {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string"
                  }
                }
              }
            ]
          }
        }
      },
      "ChatMedia": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "media_type": {
            "type": "string",
            "enum": [
              "image",
              "gif",
              "video",
              "file"
            ]
          },
          "mime_type": {
            "type": "string"
          },
          "filename": {
            "type": "string"
          },
          "byte_size": {
            "type": "integer"
          },
          "processing_status": {
            "type": "string",
            "enum": [
              "pending",
              "ready",
              "failed"
            ]
          },
          "width_px": {
            "type": "integer",
            "nullable": true
          },
          "height_px": {
            "type": "integer",
            "nullable": true
          },
          "url": {
            "type": "string",
            "nullable": true
          },
          "thumbnail_url": {
            "type": "string",
            "nullable": true
          },
          "hls_playlist_url": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "ChatSettings": {
        "type": "object",
        "description": "Business-level chat configuration (all keys optional on PATCH).",
        "properties": {
          "direct_messages_enabled": {
            "type": "boolean"
          },
          "group_chats_enabled": {
            "type": "boolean"
          },
          "allow_edit_chat_messages": {
            "type": "boolean"
          },
          "file_uploads_enabled": {
            "type": "boolean"
          },
          "max_attachment_size_mb": {
            "type": "integer",
            "minimum": 1,
            "maximum": 500
          },
          "max_video_attachment_size_mb": {
            "type": "integer"
          },
          "typing_indicators": {
            "type": "boolean"
          },
          "important_messages_enabled": {
            "type": "boolean"
          },
          "read_receipt_requests_enabled": {
            "type": "boolean"
          },
          "semantic_search_enabled": {
            "type": "boolean"
          },
          "chat_video_subtitles_enabled": {
            "type": "boolean"
          },
          "working_hours_start": {
            "type": "integer"
          },
          "working_hours_end": {
            "type": "integer"
          }
        }
      },
      "ChatPusherConfig": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "nullable": true
          },
          "cluster": {
            "type": "string"
          },
          "enabled": {
            "type": "boolean"
          },
          "user_id": {
            "type": "integer"
          },
          "business_id": {
            "type": "integer"
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "Machine readable error code"
              },
              "message": {
                "type": "string",
                "description": "Human readable error message"
              },
              "details": {
                "type": "object",
                "description": "Additional error context"
              }
            }
          }
        },
        "required": [
          "error"
        ]
      },
      "ValidationErrors": {
        "type": "object",
        "properties": {
          "errors": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "field": {
                  "type": "string",
                  "description": "Field name with validation error"
                },
                "message": {
                  "type": "string",
                  "description": "Validation error message"
                }
              }
            }
          }
        }
      },
      "ChangelogEntry": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 1
          },
          "version": {
            "type": "string",
            "example": "1.6.2"
          },
          "title": {
            "type": "string",
            "example": "Improved Agent tools and performance"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "example": "Major improvements to AI Agent experience"
          },
          "category": {
            "type": "string",
            "enum": [
              "general",
              "feature",
              "improvement",
              "fix",
              "patch"
            ],
            "example": "feature"
          },
          "priority": {
            "type": "integer",
            "enum": [
              0,
              1,
              2
            ],
            "example": 1,
            "description": "0=Normal, 1=Important, 2=Critical"
          },
          "published": {
            "type": "boolean",
            "example": true
          },
          "release_date": {
            "type": "string",
            "format": "date",
            "example": "2025-09-11"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2025-09-16T22:34:26Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2025-09-16T22:34:26Z"
          },
          "created_by": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer",
                "example": 1
              },
              "name": {
                "type": "string",
                "example": "System Admin"
              }
            }
          },
          "public_url": {
            "type": "string",
            "nullable": true,
            "example": "https://workforce.mangoapps.com/changelog/1"
          }
        }
      },
      "ChangelogEntryInput": {
        "type": "object",
        "required": [
          "version",
          "title",
          "content",
          "release_date"
        ],
        "properties": {
          "version": {
            "type": "string",
            "example": "1.7",
            "description": "Main version number (e.g., 1.6, 2.0)"
          },
          "patch_version": {
            "type": "string",
            "nullable": true,
            "example": "1",
            "description": "Patch number for minor releases (optional)"
          },
          "title": {
            "type": "string",
            "example": "Enhanced AI capabilities and performance improvements",
            "description": "Title of the changelog entry"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "example": "Next major release with improved AI models",
            "description": "Brief description of the release (optional)"
          },
          "content": {
            "type": "string",
            "example": "### New Features\n\n* Support for GPT-5 models\n* Improved code generation accuracy\n\n### Performance Improvements\n\n* 50% faster response times\n* Reduced memory usage\n",
            "description": "Markdown content of the changelog entry"
          },
          "category": {
            "type": "string",
            "enum": [
              "general",
              "feature",
              "improvement",
              "fix",
              "patch"
            ],
            "default": "general",
            "example": "feature"
          },
          "priority": {
            "type": "integer",
            "enum": [
              0,
              1,
              2
            ],
            "default": 0,
            "example": 1,
            "description": "0=Normal, 1=Important, 2=Critical"
          },
          "published": {
            "type": "boolean",
            "default": false,
            "example": true,
            "description": "Whether the entry should be published immediately"
          },
          "release_date": {
            "type": "string",
            "format": "date",
            "example": "2025-10-01",
            "description": "Release date of the version"
          },
          "metadata": {
            "type": "object",
            "nullable": true,
            "example": {},
            "description": "Additional metadata (optional)"
          }
        }
      },
      "ChangelogEntryResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 1
          },
          "version": {
            "type": "string",
            "example": "1.7.1"
          },
          "title": {
            "type": "string",
            "example": "Enhanced AI capabilities and performance improvements"
          },
          "published": {
            "type": "boolean",
            "example": true
          },
          "release_date": {
            "type": "string",
            "format": "date",
            "example": "2025-10-01"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2025-09-16T22:34:26Z"
          },
          "public_url": {
            "type": "string",
            "nullable": true,
            "example": "https://workforce.mangoapps.com/changelog/1"
          }
        }
      },
      "RecognitionPerson": {
        "type": "object",
        "description": "A person as every Recognitions list names them. Deliberately identity-only — these payloads go to every employee, and the cards they mirror show a name and a face, nothing else. An automated lifecycle award reports a null `id` with the name `System (Automated)` rather than leaking the system principal.\n",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "nullable": true,
            "example": 412
          },
          "name": {
            "type": "string",
            "example": "Patrick Smith"
          },
          "title": {
            "type": "string",
            "nullable": true,
            "description": "Job title, when the person has one on file.",
            "example": "Store Manager"
          },
          "image": {
            "type": "string",
            "nullable": true,
            "description": "Absolute avatar URL (native clients can't resolve a relative path), or null when it can't be resolved.",
            "example": "https://acme.workforce.mangoapps.com/rails/active_storage/blobs/redirect/ey.../a.jpg"
          }
        }
      },
      "WikiDashboardRow": {
        "type": "object",
        "description": "One wiki row in a Wikis dashboard list. Matches the fields the web dashboard row renders: `icon` always carries a usable FontAwesome class (falling back to `fas fa-book`) and `color` is whitelist-coerced to a hex value, so clients never receive an unusable icon or colour.\n",
        "required": [
          "id",
          "title",
          "icon",
          "color"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 17
          },
          "title": {
            "type": "string",
            "example": "Shift Swap Guidelines"
          },
          "icon": {
            "type": "string",
            "description": "FontAwesome class; defaults to `fas fa-book`.",
            "example": "fas fa-book-open"
          },
          "color": {
            "type": "string",
            "description": "Hex colour (whitelist-coerced; falls back to `#6c757d`).",
            "example": "#3b7ddd"
          }
        }
      },
      "WikiCard": {
        "type": "object",
        "description": "One wiki's display fields for the Wikis list. `icon` falls back to `fas fa-book` and `color` is whitelist-coerced to a safe hex, so clients never receive an unusable value. `bookmarked` is the calling user's bookmark state for this wiki, resolved from the same store as the `?filter=bookmarked` scope — so a list screen can draw a filled or unfilled bookmark per row without a detail call per card.\n",
        "required": [
          "id",
          "title",
          "icon",
          "color",
          "bookmarked"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 42
          },
          "title": {
            "type": "string",
            "example": "Employee Handbook"
          },
          "icon": {
            "type": "string",
            "example": "fas fa-book"
          },
          "color": {
            "type": "string",
            "example": "#2E7D32"
          },
          "bookmarked": {
            "type": "boolean",
            "description": "Whether the calling user has bookmarked this wiki.",
            "example": true
          },
          "pinned": {
            "type": "boolean",
            "deprecated": true,
            "description": "DEPRECATED mirror of `bookmarked`, kept for native builds shipped before the pin→bookmark rename. Computed from the same set, so the two can never disagree. New integrations read `bookmarked`.",
            "example": true
          }
        }
      },
      "WikiSearchHit": {
        "description": "One wiki matched by GET /wikis/search — the list card fields plus the wiki's description, its creator (id + name) and its tags. `description` is null when the wiki has none (it is one of the columns the search matches on). `creator_id` is the wiki's author; `creator_name` is null when that user no longer exists. `tags` is an empty array when untagged.\n",
        "allOf": [
          {
            "$ref": "#/components/schemas/WikiCard"
          },
          {
            "type": "object",
            "required": [
              "description",
              "creator_id",
              "creator_name",
              "tags"
            ],
            "properties": {
              "description": {
                "type": "string",
                "nullable": true,
                "description": "The wiki's short description (one of the searched columns).",
                "example": "Runbook for the deploy pipeline"
              },
              "creator_id": {
                "type": "integer",
                "nullable": true,
                "description": "Id of the user who created the wiki (`created_by_id`).",
                "example": 49290
              },
              "creator_name": {
                "type": "string",
                "nullable": true,
                "example": "Dana Lee"
              },
              "tags": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "example": [
                  "devops",
                  "runbook"
                ]
              }
            }
          }
        ]
      },
      "WikiSubTreeNode": {
        "description": "One node in a wiki's FULL sub-wiki tree — the card fields plus the page status and, RECURSIVELY, that node's own published visible sub-wikis (empty at the leaves). Used by GET /wikis/{id} (`sub_wikis`).\n",
        "allOf": [
          {
            "$ref": "#/components/schemas/WikiCard"
          },
          {
            "type": "object",
            "required": [
              "status",
              "sub_wikis"
            ],
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "draft",
                  "published",
                  "archived"
                ],
                "example": "published"
              },
              "sub_wikis": {
                "type": "array",
                "description": "This node's own sub-wikis, same shape, recursively.",
                "items": {
                  "$ref": "#/components/schemas/WikiSubTreeNode"
                }
              }
            }
          }
        ]
      },
      "IdeaComment": {
        "type": "object",
        "description": "One comment (or reply) on an idea — text + @mentions only, with no attachments (matching the web composer, which posts a bare `comment[body]` with no file field). Threading is ONE level: a top-level comment inlines its direct replies, and a reply always has `replies: []`. Soft-deleted comments never appear.\n\n**`replies` is capped at 20 per comment.** `per_page` bounds the top-level list only, so a busy thread would otherwise return every reply it has. `replies_count` is the TRUE total (it can exceed `replies.length`) and `replies_truncated` tells you the array is partial — page the rest with `GET /ideas/{idea_id}/comments?parent_comment_id={id}`, whose default `per_page` is the same 20, so page 2 continues exactly where `replies` stopped.\n",
        "required": [
          "id",
          "idea_id",
          "parent_comment_id",
          "author",
          "body",
          "mentioned_user_ids",
          "created_at",
          "replies_count",
          "replies_truncated",
          "replies",
          "can_edit",
          "can_delete"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 8842
          },
          "idea_id": {
            "type": "integer",
            "example": 42
          },
          "parent_comment_id": {
            "type": "integer",
            "nullable": true,
            "description": "The comment this replies to; null for a top-level comment.",
            "example": null
          },
          "author": {
            "type": "object",
            "required": [
              "id",
              "name"
            ],
            "properties": {
              "id": {
                "type": "integer",
                "example": 49290
              },
              "name": {
                "type": "string",
                "example": "Casey Poster"
              },
              "photo": {
                "type": "string",
                "nullable": true,
                "description": "Absolute avatar URL."
              }
            }
          },
          "body": {
            "type": "string",
            "description": "The comment text, with raw `@[Name](mention:id)` mention tokens preserved so a client can linkify them.",
            "example": "Great idea @[Dana Lee](mention:51102)"
          },
          "mentioned_user_ids": {
            "type": "array",
            "description": "Deduped ids of the users tagged in `body`.",
            "items": {
              "type": "integer"
            },
            "example": [
              51102
            ]
          },
          "edited_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-07-31T09:30:28Z"
          },
          "replies_count": {
            "type": "integer",
            "description": "The TRUE number of direct replies, from a COUNT — not the length of `replies`, which is capped at 20. Always 0 on a reply (one level of threading). Compare it with `replies.length`, or just read `replies_truncated`, to know whether there is more to fetch.",
            "example": 2
          },
          "replies_truncated": {
            "type": "boolean",
            "description": "True when `replies` holds only the first 20 of `replies_count` replies. Fetch the remainder with `GET /ideas/{idea_id}/comments?parent_comment_id={id}&page=2`. Always false on a reply.",
            "example": false
          },
          "replies": {
            "type": "array",
            "maxItems": 20,
            "description": "The direct replies, oldest first, CAPPED at 20 — see `replies_count` and `replies_truncated`. Always empty on a reply (one level of threading).",
            "items": {
              "$ref": "#/components/schemas/IdeaComment"
            }
          },
          "can_edit": {
            "type": "boolean",
            "description": "Whether the caller may PATCH this comment — the author alone, with no time window.",
            "example": true
          },
          "can_delete": {
            "type": "boolean",
            "description": "Whether the caller may delete it — the author OR an Ideas admin, with no time window.",
            "example": true
          }
        }
      },
      "IdeaVoteState": {
        "type": "object",
        "description": "An idea's vote state after a cast/remove, returned by POST/DELETE /ideas/{idea_id}/vote. `vote_count` / `has_voted` match the feed card's key names so a client can patch its cached card in place.\n",
        "required": [
          "idea"
        ],
        "properties": {
          "unread_notification_count": {
            "$ref": "#/components/schemas/UnreadNotificationCount"
          },
          "idea": {
            "type": "object",
            "required": [
              "id",
              "vote_count",
              "has_voted",
              "voting_closed",
              "vote_change_allowed"
            ],
            "properties": {
              "id": {
                "type": "integer",
                "example": 42
              },
              "vote_count": {
                "type": "integer",
                "description": "The idea's total upvotes after this change (denormalized up_votes_count).",
                "example": 18
              },
              "has_voted": {
                "type": "boolean",
                "description": "Whether the CALLER now has an upvote on this idea.",
                "example": true
              },
              "voting_closed": {
                "type": "boolean",
                "description": "Whether the idea's voting-close date has passed (no further casts).",
                "example": false
              },
              "vote_change_allowed": {
                "type": "boolean",
                "description": "Whether the \"Allow removing an upvote\" workspace setting is on. When false, DELETE /ideas/{idea_id}/vote returns 403 — the client should hide the remove-vote affordance.",
                "example": true
              }
            }
          },
          "_meta": {
            "$ref": "#/components/schemas/ResponseMeta"
          }
        }
      },
      "WikiCommentAttachment": {
        "type": "object",
        "description": "One file attached to a wiki comment.",
        "required": [
          "id",
          "filename",
          "content_type",
          "byte_size",
          "image",
          "url",
          "download_url"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 2634
          },
          "filename": {
            "type": "string",
            "example": "notes.txt"
          },
          "content_type": {
            "type": "string",
            "example": "text/plain"
          },
          "byte_size": {
            "type": "integer",
            "example": 16
          },
          "image": {
            "type": "boolean",
            "description": "True for image blobs (render as a thumbnail).",
            "example": false
          },
          "url": {
            "type": "string",
            "description": "Absolute ActiveStorage URL for opening the attachment. An IMAGE is served `Content-Disposition: inline` so it can be previewed in place; every other file — a PDF above all — is served `Content-Disposition: attachment`, because a client with no built-in PDF renderer can neither display an inline PDF nor hand it to a download, so the tap does nothing at all.\n",
            "example": "https://officechat.workforce.mangoapps.com/rails/active_storage/blobs/redirect/…/notes.txt?disposition=attachment"
          },
          "download_url": {
            "type": "string",
            "description": "Absolute URL that ALWAYS responds `Content-Disposition: attachment` — the target for an explicit Download action, images included. Identical to `url` for every non-image attachment.\n",
            "example": "https://officechat.workforce.mangoapps.com/rails/active_storage/blobs/redirect/…/notes.txt?disposition=attachment"
          }
        }
      },
      "WikiComment": {
        "type": "object",
        "description": "A wiki comment. Top-level comments inline their direct replies in `replies` (ONE level — a reply's own `replies` is always empty).\n",
        "required": [
          "id",
          "wiki_id",
          "parent_comment_id",
          "author",
          "body",
          "mentioned_user_ids",
          "created_at",
          "replies_count",
          "replies",
          "reactions",
          "attachments",
          "can_edit",
          "can_delete"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 22
          },
          "wiki_id": {
            "type": "integer",
            "example": 42
          },
          "parent_comment_id": {
            "type": "integer",
            "nullable": true,
            "description": "Null for a top-level comment; the parent's id for a reply.",
            "example": null
          },
          "author": {
            "type": "object",
            "nullable": true,
            "description": "Null if the author was deleted.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 1884
              },
              "name": {
                "type": "string",
                "example": "Georgia Fitzpatrick"
              },
              "avatar_url": {
                "type": "string",
                "example": "https://…/avatar.png"
              }
            }
          },
          "body": {
            "type": "string",
            "description": "Comment text with raw `@[Name](mention:id)` mention tokens preserved.",
            "example": "Great page @[Casey Poster](mention:49290)"
          },
          "mentioned_user_ids": {
            "type": "array",
            "description": "Deduped user ids mentioned in the body.",
            "items": {
              "type": "integer"
            },
            "example": [
              49290
            ]
          },
          "edited_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "replies_count": {
            "type": "integer",
            "description": "Number of direct replies (0 on a reply).",
            "example": 2
          },
          "replies": {
            "type": "array",
            "description": "Direct replies, oldest first. Empty on a reply (one level deep).",
            "items": {
              "$ref": "#/components/schemas/WikiComment"
            }
          },
          "reactions": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer",
                "example": 3
              },
              "top_emojis": {
                "type": "array",
                "description": "Up to 3 emojis by count, highest first.",
                "items": {
                  "type": "object",
                  "properties": {
                    "emoji": {
                      "type": "string",
                      "example": "👍"
                    },
                    "count": {
                      "type": "integer",
                      "example": 2
                    }
                  }
                }
              },
              "my_reactions": {
                "type": "array",
                "description": "Emojis the calling user reacted with.",
                "items": {
                  "type": "string"
                },
                "example": [
                  "👍"
                ]
              }
            }
          },
          "attachments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WikiCommentAttachment"
            }
          },
          "can_edit": {
            "type": "boolean",
            "description": "Whether the CALLING user may edit this comment: an admin always may; otherwise only the author, and only within 15 minutes of posting.\n",
            "example": true
          },
          "can_delete": {
            "type": "boolean",
            "description": "Whether the CALLING user may delete this comment: an admin always may; otherwise only the author, and only within 5 minutes of posting.\n",
            "example": false
          }
        }
      },
      "ResponseMeta": {
        "type": "object",
        "description": "Standard response metadata",
        "additionalProperties": true,
        "properties": {
          "request_id": {
            "type": "string",
            "description": "Unique request identifier"
          },
          "generated_at": {
            "type": "string",
            "format": "date-time",
            "description": "Timestamp when the response was generated"
          },
          "execution_time_ms": {
            "type": "number",
            "description": "Server processing time in milliseconds"
          },
          "total_count": {
            "type": "integer",
            "description": "Total number of items"
          },
          "total_pages": {
            "type": "integer",
            "description": "Total number of pages"
          },
          "current_page": {
            "type": "integer",
            "description": "Current page number"
          },
          "per_page": {
            "type": "integer",
            "description": "Number of items per page"
          }
        }
      },
      "User": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 1
          },
          "uid": {
            "type": "string",
            "description": "Unique employee identifier (mango_employee_id)",
            "example": "E1234567",
            "nullable": true
          },
          "email": {
            "type": "string",
            "format": "email",
            "example": "user@example.com"
          },
          "first_name": {
            "type": "string",
            "example": "John"
          },
          "last_name": {
            "type": "string",
            "example": "Doe"
          },
          "full_name": {
            "type": "string",
            "example": "John Doe"
          },
          "role": {
            "type": "string",
            "example": "employee"
          },
          "active": {
            "type": "boolean",
            "example": true
          },
          "avatar_url": {
            "type": "string",
            "format": "uri",
            "nullable": true
          },
          "phone": {
            "type": "string",
            "nullable": true
          },
          "job_title": {
            "type": "string",
            "nullable": true,
            "example": "Software Engineer"
          },
          "department": {
            "type": "string",
            "nullable": true,
            "example": "Engineering"
          },
          "organizational_role": {
            "type": "string",
            "nullable": true,
            "example": "Senior Developer"
          },
          "hire_date": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": "2023-01-15T00:00:00Z"
          },
          "employment_status": {
            "type": "string",
            "nullable": true,
            "example": "full_time"
          },
          "employee_type": {
            "type": "string",
            "nullable": true,
            "example": "Full-Time"
          },
          "manager": {
            "type": "object",
            "nullable": true,
            "properties": {
              "uid": {
                "type": "string",
                "example": "E1234567"
              },
              "name": {
                "type": "string",
                "example": "Jane Smith"
              },
              "email": {
                "type": "string",
                "format": "email",
                "example": "jane.smith@example.com"
              }
            }
          },
          "locations": {
            "type": "array",
            "description": "Enhanced location information with IDs (NEW: includes id, name, and address)",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "description": "Location ID for reference in other API calls",
                  "example": 265
                },
                "name": {
                  "type": "string",
                  "description": "Location name",
                  "example": "Issaquah Office"
                },
                "address": {
                  "type": "string",
                  "nullable": true,
                  "description": "Physical address of the location",
                  "example": "1495 11th Avenue Northwest"
                }
              }
            }
          },
          "marketplace_apps": {
            "type": "array",
            "description": "Marketplace apps enabled for the user's business (NEW)",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "description": "Marketplace app ID",
                  "example": 6
                },
                "name": {
                  "type": "string",
                  "description": "App display name",
                  "example": "Performance Management"
                },
                "slug": {
                  "type": "string",
                  "description": "App URL slug",
                  "example": "employee-performance-management"
                },
                "category": {
                  "type": "string",
                  "description": "App category",
                  "example": "performance"
                }
              }
            }
          },
          "termination_date": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "termination_type": {
            "type": "string",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2023-01-01T00:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2024-01-01T00:00:00Z"
          },
          "last_activity_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "preferences": {
            "type": "object",
            "description": "User preferences"
          }
        }
      },
      "Business": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "subdomain": {
            "type": "string"
          },
          "timezone": {
            "type": "string",
            "example": "UTC"
          },
          "logo_url": {
            "type": "string",
            "format": "uri",
            "nullable": true
          },
          "branding": {
            "type": "object",
            "nullable": true,
            "description": "Branding color configuration for mobile theming (HEX format)",
            "properties": {
              "primary_button_color": {
                "type": "string",
                "example": "#2e63b3"
              },
              "secondary_button_color": {
                "type": "string",
                "example": "#6c757d"
              },
              "status_success_color": {
                "type": "string",
                "example": "#198754"
              },
              "status_error_color": {
                "type": "string",
                "example": "#dc3545"
              },
              "status_warning_color": {
                "type": "string",
                "example": "#ffc107"
              },
              "status_info_color": {
                "type": "string",
                "example": "#0dcaf0"
              },
              "mobile_header_background": {
                "type": "string",
                "example": "#2e63b3"
              },
              "mobile_header_text": {
                "type": "string",
                "example": "#FFFFFF"
              },
              "mobile_footer_background": {
                "type": "string",
                "example": "#FFFFFF"
              },
              "mobile_footer_icon_active": {
                "type": "string",
                "example": "#2e63b3"
              },
              "mobile_footer_icon_inactive": {
                "type": "string",
                "example": "#6c757d"
              }
            }
          },
          "settings": {
            "type": "object",
            "description": "Business settings for mobile app"
          }
        }
      },
      "Shift": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 123
          },
          "name": {
            "type": "string",
            "example": "Morning Shift"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "start_time": {
            "type": "string",
            "format": "date-time",
            "example": "2025-01-27T09:00:00Z"
          },
          "end_time": {
            "type": "string",
            "format": "date-time",
            "example": "2025-01-27T17:00:00Z"
          },
          "formatted_date": {
            "type": "string",
            "description": "Human-readable date format",
            "example": "Monday, Jan 27"
          },
          "formatted_time": {
            "type": "string",
            "description": "Human-readable time range",
            "example": "9:00 AM - 5:00 PM"
          },
          "status": {
            "type": "string",
            "enum": [
              "scheduled",
              "completed",
              "cancelled",
              "open"
            ],
            "example": "scheduled"
          },
          "urgent": {
            "type": "boolean",
            "description": "Whether this shift is marked as urgent",
            "example": false
          },
          "needs_coverage": {
            "type": "boolean",
            "description": "Whether this shift needs coverage",
            "example": false
          },
          "can_list_for_coverage": {
            "type": "boolean",
            "description": "Whether the current user can list this shift for coverage in the marketplace",
            "example": true
          },
          "is_already_listed": {
            "type": "boolean",
            "description": "Whether this shift is already listed in the marketplace by the current user",
            "example": false
          },
          "required_users": {
            "type": "integer",
            "description": "Number of users required for this shift"
          },
          "location": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "example": 1
              },
              "name": {
                "type": "string",
                "example": "Downtown Store"
              },
              "address": {
                "type": "string",
                "nullable": true
              },
              "phone": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "users": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/User"
            }
          },
          "available_actions": {
            "type": "array",
            "description": "Actions available to the current user for this shift",
            "items": {
              "type": "string",
              "enum": [
                "view_details",
                "request_coverage",
                "report_lateness",
                "view_teammates",
                "claim"
              ]
            },
            "example": [
              "view_details",
              "request_coverage",
              "view_teammates"
            ]
          },
          "teammates": {
            "type": "array",
            "description": "Other users assigned to this shift (when include=teammates)",
            "nullable": true,
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "description": "User ID"
                },
                "name": {
                  "type": "string",
                  "description": "User full name"
                },
                "job_title": {
                  "type": "string",
                  "nullable": true,
                  "description": "User's job title/role"
                },
                "profile_photo_url": {
                  "type": "string",
                  "nullable": true,
                  "description": "Full-size profile photo URL (200x200) or avatar placeholder"
                },
                "profile_photo_thumbnail_url": {
                  "type": "string",
                  "nullable": true,
                  "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                }
              }
            }
          },
          "listed_by": {
            "type": "object",
            "nullable": true,
            "description": "User who listed this shift in the marketplace (teammate format, present for marketplace shifts)",
            "properties": {
              "id": {
                "type": "integer",
                "description": "User ID"
              },
              "name": {
                "type": "string",
                "description": "User full name"
              },
              "job_title": {
                "type": "string",
                "nullable": true,
                "description": "User's job title/role"
              },
              "profile_photo_url": {
                "type": "string",
                "nullable": true,
                "description": "Full-size profile photo URL (200x200) or avatar placeholder"
              },
              "profile_photo_thumbnail_url": {
                "type": "string",
                "nullable": true,
                "description": "Thumbnail profile photo URL (40x40) optimized for list views"
              }
            }
          },
          "picked_by": {
            "type": "object",
            "nullable": true,
            "description": "User who claimed/picked up this shift from the marketplace (teammate format, present when shift was claimed)",
            "properties": {
              "id": {
                "type": "integer",
                "description": "User ID"
              },
              "name": {
                "type": "string",
                "description": "User full name"
              },
              "job_title": {
                "type": "string",
                "nullable": true,
                "description": "User's job title/role"
              },
              "profile_photo_url": {
                "type": "string",
                "nullable": true,
                "description": "Full-size profile photo URL (200x200) or avatar placeholder"
              },
              "profile_photo_thumbnail_url": {
                "type": "string",
                "nullable": true,
                "description": "Thumbnail profile photo URL (40x40) optimized for list views"
              }
            }
          },
          "marketplace_info": {
            "type": "object",
            "nullable": true,
            "description": "Marketplace listing information (present when shift has marketplace listing)",
            "properties": {
              "has_listing": {
                "type": "boolean",
                "description": "Whether this shift has an active marketplace listing",
                "example": true
              },
              "listing_id": {
                "type": "integer",
                "description": "ID of the marketplace listing",
                "example": 880
              },
              "listing_status": {
                "type": "string",
                "enum": [
                  "open",
                  "filled",
                  "closed",
                  "cancelled"
                ],
                "description": "Current status of the marketplace listing",
                "example": "open"
              },
              "listing_type": {
                "type": "string",
                "enum": [
                  "pickup",
                  "trade_only",
                  "both"
                ],
                "description": "Type of marketplace listing - pickup (direct claim), trade_only (requires application/trade), or both (accepts either)",
                "example": "pickup"
              },
              "price": {
                "type": "number",
                "format": "float",
                "description": "Price offered for the shift",
                "example": 0.01
              },
              "currency": {
                "type": "string",
                "description": "Currency code (e.g., USD)",
                "example": "USD"
              },
              "listed_by": {
                "type": "integer",
                "description": "User ID of the person who listed the shift (legacy field, use listed_by object instead)",
                "example": 1487
              },
              "listed_at": {
                "type": "string",
                "format": "date-time",
                "description": "When the shift was listed in the marketplace",
                "example": "2025-12-01T10:05:00Z"
              }
            }
          },
          "notes": {
            "type": "string",
            "nullable": true,
            "description": "Shift notes (when include=notes)"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AttendanceRecord": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "user_id": {
            "type": "integer"
          },
          "shift_id": {
            "type": "integer"
          },
          "business_id": {
            "type": "integer"
          },
          "check_in_time": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "check_out_time": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "on_time",
              "late",
              "missed",
              "completed",
              "absence_reported",
              "excused",
              "no_show"
            ]
          },
          "location_verified": {
            "type": "boolean",
            "default": false
          },
          "photo_verification_required": {
            "type": "boolean",
            "default": false
          },
          "photo_verification_failed_reason": {
            "type": "string",
            "nullable": true
          },
          "is_unknown_device": {
            "type": "boolean",
            "default": false
          },
          "requires_review": {
            "type": "boolean",
            "default": false
          },
          "review_reason": {
            "type": "string",
            "nullable": true
          },
          "reviewed_by_user_id": {
            "type": "integer",
            "nullable": true
          },
          "reviewed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "excused_by_user_id": {
            "type": "integer",
            "nullable": true
          },
          "excused_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "absence_report_id": {
            "type": "integer",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "user": {
            "$ref": "#/components/schemas/User"
          },
          "shift": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Shift"
              }
            ],
            "nullable": true,
            "description": "The record's shift. A SUBSET of the Shift schema: id, name, start_time, end_time, formatted_date, formatted_time — plus status, location and location_department when `include=shift_details` is requested. `formatted_date` (\"Wednesday, Aug 05\") and `formatted_time` (\"01:30 PM - 09:30 PM\") are rendered in the SHIFT's own timezone, are byte-identical to what `GET /shifts` returns for the same shift.id, and do not vary with the caller's profile timezone — clients should prefer them over formatting `start_time` locally, which has no usable zone once an SDK decodes it to a bare instant. `formatted_date` always names the shift's START day, so it is correct for a shift crossing midnight."
          },
          "break_records": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BreakRecord"
            }
          }
        }
      },
      "BreakRecord": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "attendance_record_id": {
            "type": "integer"
          },
          "break_type_id": {
            "type": "integer"
          },
          "start_time": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "end_time": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "duration_minutes": {
            "type": "integer",
            "nullable": true
          },
          "required_duration_minutes": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "in_progress",
              "completed",
              "skipped",
              "interrupted"
            ]
          },
          "notes": {
            "type": "string",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "MissingPunchRequest": {
        "type": "object",
        "description": "An employee-submitted punch correction. Stored as an AttendanceRecord with punch_source='missing_punch_request' and flagged for manager review — it is a REQUEST, not an approved punch, until a manager acts on it.\n",
        "properties": {
          "id": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "description": "`pending_review` until a manager acts; afterwards the underlying attendance status.\n"
          },
          "pending_review": {
            "type": "boolean"
          },
          "check_in_time": {
            "type": "string",
            "format": "date-time"
          },
          "check_out_time": {
            "type": "string",
            "format": "date-time"
          },
          "reason": {
            "type": "string",
            "description": "The employee's stated reason for the correction."
          },
          "reviewed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "can_cancel": {
            "type": "boolean",
            "description": "Whether DELETE would succeed — true only while no manager has acted. Use it to decide whether to show a cancel control instead of discovering the rule from a 422.\n"
          },
          "shift_id": {
            "type": "integer",
            "description": "Detailed responses only (create). The supporting ad-hoc shift."
          },
          "location_name": {
            "type": "string",
            "nullable": true,
            "description": "Detailed responses only."
          },
          "breaks": {
            "type": "array",
            "description": "Detailed responses only. Required break types the employee did not submit appear with status `skipped`.\n",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "break_type_id": {
                  "type": "integer"
                },
                "break_type_name": {
                  "type": "string"
                },
                "start_time": {
                  "type": "string",
                  "format": "date-time",
                  "nullable": true
                },
                "end_time": {
                  "type": "string",
                  "format": "date-time",
                  "nullable": true
                },
                "duration_minutes": {
                  "type": "integer",
                  "nullable": true
                },
                "status": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "ShiftFeedback": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 789
          },
          "attendance_record_id": {
            "type": "integer",
            "example": 123,
            "description": "ID of the associated attendance record"
          },
          "user_id": {
            "type": "integer",
            "example": 456,
            "description": "ID of the user who submitted feedback"
          },
          "rating": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5,
            "example": 4,
            "description": "Overall shift rating (1-5 stars)"
          },
          "feedback_text": {
            "type": "string",
            "nullable": true,
            "example": "Great shift, but the workspace was a bit noisy.",
            "description": "Detailed feedback text (optional)"
          },
          "shift_difficulty": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5,
            "nullable": true,
            "example": 3,
            "description": "How difficult was the shift (1=Easy, 5=Very Hard)"
          },
          "would_work_again": {
            "type": "boolean",
            "nullable": true,
            "example": true,
            "description": "Would the user work this shift again"
          },
          "can_edit": {
            "type": "boolean",
            "example": true,
            "description": "Whether the feedback can still be edited (within 24 hours)"
          },
          "edit_window_expires_at": {
            "type": "string",
            "format": "date-time",
            "example": "2025-10-11T16:00:00Z",
            "description": "When the edit window expires (24 hours from submission)"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2025-10-10T16:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2025-10-10T16:00:00Z"
          },
          "attendance_record": {
            "$ref": "#/components/schemas/AttendanceRecord"
          },
          "shift": {
            "$ref": "#/components/schemas/Shift"
          },
          "user": {
            "$ref": "#/components/schemas/User"
          }
        }
      },
      "ShiftMarketplaceListing": {
        "type": "object",
        "description": "A marketplace listing for shifts (pickup, trade_only, or both)",
        "properties": {
          "id": {
            "type": "integer",
            "example": 1
          },
          "listing_type": {
            "type": "string",
            "enum": [
              "pickup",
              "trade_only",
              "both"
            ],
            "example": "pickup",
            "description": "Type of listing - pickup (direct claim), trade_only (requires application), or both"
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "filled",
              "closed",
              "cancelled"
            ],
            "example": "open"
          },
          "price": {
            "type": "number",
            "nullable": true,
            "example": 0.01
          },
          "currency": {
            "type": "string",
            "example": "USD"
          },
          "notes": {
            "type": "string",
            "nullable": true,
            "example": "Need someone to cover this shift"
          },
          "urgent": {
            "type": "boolean",
            "description": "Whether this listing is marked as urgent",
            "example": false
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "claimed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "shift": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "start_time": {
                "type": "string",
                "format": "date-time"
              },
              "end_time": {
                "type": "string",
                "format": "date-time"
              },
              "date": {
                "type": "string",
                "format": "date"
              },
              "formatted_date": {
                "type": "string"
              },
              "formatted_time": {
                "type": "string"
              },
              "location": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  }
                }
              }
            }
          },
          "created_by": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string"
              },
              "avatar_url": {
                "type": "string",
                "nullable": true,
                "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
              },
              "avatar_thumbnail_url": {
                "type": "string",
                "nullable": true,
                "description": "Thumbnail profile photo URL (40x40) optimized for list views"
              }
            }
          },
          "claimed_by": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string"
              },
              "avatar_url": {
                "type": "string",
                "nullable": true,
                "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
              },
              "avatar_thumbnail_url": {
                "type": "string",
                "nullable": true,
                "description": "Thumbnail profile photo URL (40x40) optimized for list views"
              }
            }
          },
          "can_claim": {
            "type": "boolean",
            "description": "Whether current user can claim this listing"
          },
          "accepts_applications": {
            "type": "boolean",
            "description": "Whether this listing accepts applications (trade_only or both)"
          },
          "is_trade_only": {
            "type": "boolean",
            "description": "Whether this is a trade-only listing"
          },
          "has_applied": {
            "type": "boolean",
            "description": "Whether current user has applied to this listing"
          },
          "application_status": {
            "type": "string",
            "nullable": true,
            "enum": [
              "pending",
              "accepted",
              "rejected",
              "withdrawn"
            ],
            "description": "Current user's application status (if applied)"
          },
          "application_id": {
            "type": "integer",
            "nullable": true,
            "description": "Current user's application ID (if applied)"
          },
          "is_owner": {
            "type": "boolean",
            "description": "Whether current user owns this listing"
          },
          "can_edit": {
            "type": "boolean"
          },
          "can_delete": {
            "type": "boolean"
          },
          "applications": {
            "type": "array",
            "description": "Owner-only application summaries for this listing",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "pending",
                    "accepted",
                    "rejected",
                    "withdrawn"
                  ]
                },
                "notes": {
                  "type": "string",
                  "nullable": true
                },
                "created_at": {
                  "type": "string",
                  "format": "date-time"
                },
                "offered_shift": {
                  "type": "object",
                  "nullable": true,
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "name": {
                      "type": "string"
                    },
                    "start_time": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "end_time": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "formatted_date": {
                      "type": "string"
                    },
                    "formatted_time": {
                      "type": "string"
                    },
                    "location": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string"
                        }
                      }
                    }
                  }
                },
                "applicant": {
                  "type": "object",
                  "nullable": true,
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "name": {
                      "type": "string"
                    },
                    "avatar_url": {
                      "type": "string",
                      "nullable": true
                    },
                    "avatar_thumbnail_url": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "applications_count": {
            "type": "integer",
            "description": "Total applications (owner only)"
          },
          "pending_applications_count": {
            "type": "integer",
            "description": "Pending applications count (owner only)"
          }
        }
      },
      "ShiftMarketplaceApplication": {
        "type": "object",
        "description": "An application to a trade listing",
        "properties": {
          "id": {
            "type": "integer",
            "example": 1
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "accepted",
              "rejected",
              "withdrawn"
            ],
            "example": "pending"
          },
          "notes": {
            "type": "string",
            "nullable": true,
            "example": "I can work this shift"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "listing": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "listing_type": {
                "type": "string"
              },
              "status": {
                "type": "string"
              },
              "shift": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  },
                  "start_time": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "end_time": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "formatted_date": {
                    "type": "string"
                  },
                  "formatted_time": {
                    "type": "string"
                  },
                  "location": {
                    "type": "object",
                    "nullable": true,
                    "properties": {
                      "id": {
                        "type": "integer"
                      },
                      "name": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "created_by": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  },
                  "email": {
                    "type": "string"
                  },
                  "avatar_url": {
                    "type": "string",
                    "nullable": true,
                    "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
                  },
                  "avatar_thumbnail_url": {
                    "type": "string",
                    "nullable": true,
                    "description": "Thumbnail profile photo URL (40x40) optimized for list views"
                  }
                }
              }
            }
          },
          "applicant": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string"
              },
              "avatar_url": {
                "type": "string",
                "nullable": true,
                "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
              },
              "avatar_thumbnail_url": {
                "type": "string",
                "nullable": true,
                "description": "Thumbnail profile photo URL (40x40) optimized for list views"
              }
            }
          },
          "is_applicant": {
            "type": "boolean",
            "description": "Whether current user is the applicant"
          },
          "is_listing_owner": {
            "type": "boolean",
            "description": "Whether current user owns the listing"
          },
          "can_accept": {
            "type": "boolean",
            "description": "Whether current user can accept this application"
          },
          "can_reject": {
            "type": "boolean",
            "description": "Whether current user can reject this application"
          },
          "can_withdraw": {
            "type": "boolean",
            "description": "Whether current user can withdraw this application"
          }
        }
      },
      "ShiftDirectOffer": {
        "type": "object",
        "description": "A direct peer-to-peer shift offer",
        "properties": {
          "id": {
            "type": "integer",
            "example": 1
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "accepted",
              "declined",
              "expired",
              "cancelled"
            ],
            "example": "pending"
          },
          "notes": {
            "type": "string",
            "nullable": true
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "shift": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "start_time": {
                "type": "string",
                "format": "date-time"
              },
              "end_time": {
                "type": "string",
                "format": "date-time"
              },
              "formatted_date": {
                "type": "string"
              },
              "formatted_time": {
                "type": "string"
              },
              "location": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "from_user": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string"
              },
              "avatar_url": {
                "type": "string",
                "nullable": true,
                "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
              },
              "avatar_thumbnail_url": {
                "type": "string",
                "nullable": true,
                "description": "Thumbnail profile photo URL (40x40) optimized for list views"
              }
            }
          },
          "to_user": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string"
              },
              "avatar_url": {
                "type": "string",
                "nullable": true,
                "description": "Full-size profile photo URL (200x200) or fallback to initials-based avatar"
              },
              "avatar_thumbnail_url": {
                "type": "string",
                "nullable": true,
                "description": "Thumbnail profile photo URL (40x40) optimized for list views"
              }
            }
          },
          "is_sender": {
            "type": "boolean",
            "description": "Whether current user sent this offer"
          },
          "is_recipient": {
            "type": "boolean",
            "description": "Whether current user received this offer"
          },
          "can_accept": {
            "type": "boolean",
            "description": "Whether current user can accept this offer"
          },
          "can_decline": {
            "type": "boolean",
            "description": "Whether current user can decline this offer"
          },
          "can_cancel": {
            "type": "boolean",
            "description": "Whether current user can cancel this offer (sender only, pending offers)"
          }
        }
      },
      "BreakType": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "business_id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "duration_minutes": {
            "type": "integer"
          },
          "is_required": {
            "type": "boolean"
          },
          "is_paid": {
            "type": "boolean"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PiggybackMeta": {
        "type": "object",
        "description": "Piggyback response metadata containing additional contextual data",
        "properties": {
          "piggyback": {
            "type": "object",
            "description": "Additional data included with the response to reduce subsequent API calls",
            "properties": {
              "business_settings": {
                "type": "object",
                "description": "Business configuration and branding information",
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  },
                  "subdomain": {
                    "type": "string"
                  },
                  "timezone": {
                    "type": "string"
                  },
                  "logo_url": {
                    "type": "string",
                    "nullable": true
                  },
                  "industry": {
                    "type": "string",
                    "nullable": true
                  },
                  "employee_count": {
                    "type": "integer"
                  },
                  "company_size": {
                    "type": "string"
                  },
                  "settings": {
                    "type": "object"
                  }
                }
              },
              "user_preferences": {
                "type": "object",
                "description": "User preferences and settings",
                "properties": {
                  "timezone": {
                    "type": "string"
                  },
                  "locale": {
                    "type": "string"
                  },
                  "date_format": {
                    "type": "string"
                  },
                  "time_format": {
                    "type": "string"
                  },
                  "notifications_enabled": {
                    "type": "boolean"
                  },
                  "default_view": {
                    "type": "string"
                  },
                  "theme": {
                    "type": "string"
                  }
                }
              },
              "recent_shifts": {
                "type": "array",
                "description": "Recent completed shifts (last 5)",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "title": {
                      "type": "string"
                    },
                    "start_time": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "end_time": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "location": {
                      "type": "string",
                      "nullable": true
                    },
                    "status": {
                      "type": "string"
                    },
                    "duration_hours": {
                      "type": "number"
                    }
                  }
                }
              },
              "upcoming_shifts": {
                "type": "array",
                "description": "Upcoming scheduled shifts (next 10)",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "title": {
                      "type": "string"
                    },
                    "start_time": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "end_time": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "location": {
                      "type": "string",
                      "nullable": true
                    },
                    "status": {
                      "type": "string"
                    },
                    "check_in_time": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "time_until_start": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              },
              "notifications": {
                "type": "array",
                "description": "Unread notifications (last 20)",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "type": {
                      "type": "string"
                    },
                    "title": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "priority": {
                      "type": "string",
                      "enum": [
                        "normal",
                        "high",
                        "urgent"
                      ]
                    },
                    "action_url": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              },
              "business_stats": {
                "type": "object",
                "description": "Business statistics (admin/manager only)",
                "properties": {
                  "total_employees": {
                    "type": "integer"
                  },
                  "shifts_this_week": {
                    "type": "integer"
                  },
                  "shifts_today": {
                    "type": "integer"
                  },
                  "attendance_rate": {
                    "type": "number"
                  },
                  "open_shifts": {
                    "type": "integer"
                  },
                  "pending_requests": {
                    "type": "integer"
                  }
                }
              },
              "feature_flags": {
                "type": "array",
                "description": "Enabled feature flags for this business",
                "items": {
                  "type": "string"
                }
              },
              "system_status": {
                "type": "object",
                "description": "System status and announcements",
                "properties": {
                  "api_version": {
                    "type": "string"
                  },
                  "server_time": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "maintenance_mode": {
                    "type": "boolean"
                  },
                  "announcements": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    }
                  }
                }
              },
              "server_recommendations": {
                "type": "object",
                "description": "AI-powered recommendations and suggested actions",
                "properties": {
                  "next_actions": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "setup_needed": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "quick_actions": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "scheduling_tips": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "notifications": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "app_tips": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "query_info": {
            "type": "object",
            "description": "Information about the piggyback data generation",
            "properties": {
              "included_data": {
                "type": "array",
                "description": "List of piggyback data types that were included",
                "items": {
                  "type": "string"
                }
              },
              "execution_time_ms": {
                "type": "number",
                "description": "Time taken to generate piggyback data in milliseconds"
              },
              "cache_hits": {
                "type": "integer",
                "description": "Number of cache hits during data generation"
              },
              "cache_misses": {
                "type": "integer",
                "description": "Number of cache misses during data generation"
              }
            }
          }
        }
      },
      "HttpEnhancementMeta": {
        "type": "object",
        "description": "HTTP protocol enhancements for client performance and dynamic behavior",
        "properties": {
          "http": {
            "type": "object",
            "description": "HTTP enhancement metadata",
            "properties": {
              "caching": {
                "type": "object",
                "description": "Intelligent caching information",
                "properties": {
                  "etag": {
                    "type": "string",
                    "description": "Entity tag for content validation",
                    "example": "W/\"abc123-def456\""
                  },
                  "max_age": {
                    "type": "integer",
                    "description": "Cache max-age in seconds",
                    "example": 300
                  },
                  "private": {
                    "type": "boolean",
                    "description": "Whether response should be cached privately",
                    "example": true
                  },
                  "revalidate": {
                    "type": "boolean",
                    "description": "Whether cache must revalidate with server",
                    "example": false
                  },
                  "vary": {
                    "type": "array",
                    "description": "Headers that affect caching",
                    "items": {
                      "type": "string"
                    },
                    "example": [
                      "Accept",
                      "Authorization"
                    ]
                  },
                  "cacheable": {
                    "type": "boolean",
                    "description": "Whether response is cacheable",
                    "example": true
                  }
                }
              },
              "navigation": {
                "type": "object",
                "description": "Dynamic navigation and preload hints",
                "properties": {
                  "next_actions": {
                    "type": "array",
                    "description": "Suggested next actions for the client",
                    "items": {
                      "type": "object",
                      "properties": {
                        "rel": {
                          "type": "string",
                          "description": "Relationship type",
                          "example": "edit-profile"
                        },
                        "href": {
                          "type": "string",
                          "description": "URL for the action",
                          "example": "/api/v1/users/me"
                        },
                        "method": {
                          "type": "string",
                          "description": "HTTP method",
                          "example": "PATCH"
                        },
                        "title": {
                          "type": "string",
                          "description": "Human-readable action title",
                          "example": "Edit Profile"
                        },
                        "preload": {
                          "type": "boolean",
                          "description": "Whether to preload this resource",
                          "example": true
                        },
                        "prefetch": {
                          "type": "boolean",
                          "description": "Whether to prefetch this resource",
                          "example": false
                        }
                      }
                    }
                  },
                  "redirects": {
                    "type": "object",
                    "description": "Conditional redirect suggestions",
                    "properties": {
                      "conditional": {
                        "type": "object",
                        "description": "Conditional redirects based on user state",
                        "additionalProperties": {
                          "type": "string"
                        },
                        "example": {
                          "incomplete_profile": "/api/v1/users/me/complete",
                          "pending_verification": "/api/v1/auth/verify"
                        }
                      }
                    }
                  },
                  "preload_hints": {
                    "type": "array",
                    "description": "Resources to preload",
                    "items": {
                      "type": "object",
                      "properties": {
                        "resource": {
                          "type": "string",
                          "example": "/api/v1/businesses/settings"
                        },
                        "as": {
                          "type": "string",
                          "example": "fetch"
                        },
                        "priority": {
                          "type": "string",
                          "enum": [
                            "high",
                            "medium",
                            "low"
                          ],
                          "example": "high"
                        }
                      }
                    }
                  }
                }
              },
              "performance": {
                "type": "object",
                "description": "Performance metrics and optimization info",
                "properties": {
                  "compression": {
                    "type": "string",
                    "description": "Compression type used",
                    "enum": [
                      "gzip",
                      "deflate",
                      "none"
                    ],
                    "example": "gzip"
                  },
                  "response_time_ms": {
                    "type": "number",
                    "description": "Server response time in milliseconds",
                    "example": 45.2
                  },
                  "cache_status": {
                    "type": "string",
                    "description": "Cache status for this request",
                    "enum": [
                      "hit",
                      "miss",
                      "conditional",
                      "bypass"
                    ],
                    "example": "hit"
                  },
                  "estimated_size_bytes": {
                    "type": "integer",
                    "description": "Estimated response size in bytes",
                    "example": 2048
                  },
                  "optimization_score": {
                    "type": "integer",
                    "description": "Performance optimization score (0-100)",
                    "example": 85
                  }
                }
              },
              "client_hints": {
                "type": "object",
                "description": "Hints for client behavior and capabilities",
                "properties": {
                  "api_version": {
                    "type": "string",
                    "description": "Current API version",
                    "example": "1.0"
                  },
                  "rate_limit": {
                    "type": "object",
                    "description": "Rate limiting information",
                    "properties": {
                      "remaining": {
                        "type": "integer",
                        "description": "Requests remaining in current window",
                        "example": 98
                      },
                      "reset": {
                        "type": "integer",
                        "description": "Unix timestamp when rate limit resets",
                        "example": 1640995200
                      },
                      "limit": {
                        "type": "integer",
                        "description": "Total requests allowed per window",
                        "example": 100
                      }
                    }
                  },
                  "deprecations": {
                    "type": "array",
                    "description": "Deprecation warnings for used features",
                    "items": {
                      "type": "object",
                      "properties": {
                        "feature": {
                          "type": "string",
                          "example": "API version 0.9"
                        },
                        "deprecated_since": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-01-01"
                        },
                        "removal_date": {
                          "type": "string",
                          "format": "date",
                          "example": "2024-12-31"
                        },
                        "replacement": {
                          "type": "string",
                          "example": "API version 1.0+"
                        }
                      }
                    }
                  },
                  "preferred_format": {
                    "type": "string",
                    "description": "Recommended response format for client",
                    "example": "application/json"
                  },
                  "capabilities": {
                    "type": "array",
                    "description": "Detected client capabilities",
                    "items": {
                      "type": "string"
                    },
                    "example": [
                      "compression",
                      "conditional_requests",
                      "mobile_optimized"
                    ]
                  }
                }
              }
            }
          }
        }
      },
      "MarketplaceApp": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique identifier for the marketplace app",
            "example": 42
          },
          "slug": {
            "type": "string",
            "description": "URL-friendly identifier for the app",
            "example": "employee-performance-management"
          },
          "name": {
            "type": "string",
            "description": "Display name of the app",
            "example": "Employee Performance Management"
          },
          "description": {
            "type": "string",
            "description": "Short description of the app",
            "example": "Comprehensive performance review and goal management system"
          },
          "icon": {
            "type": "object",
            "description": "App icon information",
            "properties": {
              "url": {
                "type": "string",
                "description": "URL to the app icon",
                "example": "https://example.com/icon.png"
              },
              "type": {
                "type": "string",
                "enum": [
                  "icon_class",
                  "url",
                  "default"
                ],
                "description": "Type of icon source. This is the authoritative, versioned contract —\nthe same three values on every app-listing endpoint:\n- icon_class: Font Awesome 5 class carried in `url`\n- url: HTTP(S) URL or asset path carried in `url`. Icons stored as\n  Active Storage attachments report as `url` too — where the server\n  keeps the image is not part of the contract.\n- default: built-in fallback asset path\nAdding a value is a BREAKING change (clients decode this as a strict\nenum), so it requires an API version bump. Server-side source of\ntruth: MobileViewSupport::API_ICON_TYPES.\n",
                "example": "url"
              }
            }
          },
          "category": {
            "type": "string",
            "description": "App category",
            "example": "hr"
          },
          "version": {
            "type": "string",
            "description": "Current version of the app",
            "example": "2.1.0"
          },
          "featured": {
            "type": "boolean",
            "description": "Whether the app is featured",
            "example": false
          },
          "sort_order": {
            "type": "integer",
            "description": "Display order for the app",
            "example": 10
          },
          "url": {
            "type": "string",
            "description": "Launch URL for the app",
            "example": "/apps/employee-performance-management"
          },
          "configuration": {
            "type": "object",
            "description": "Business-specific configuration for this app",
            "additionalProperties": true,
            "example": {
              "enable_self_reviews": true,
              "review_cycle_months": 12
            }
          },
          "enabled_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the app was enabled for this business",
            "example": "2024-01-15T10:30:00Z"
          },
          "metadata": {
            "type": "object",
            "description": "Additional app metadata",
            "additionalProperties": true,
            "example": {
              "requires_training": false,
              "setup_time_minutes": 30
            }
          }
        },
        "required": [
          "id",
          "slug",
          "name",
          "category",
          "url"
        ]
      },
      "CoreApp": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique identifier for the core app",
            "example": "shifts_scheduling"
          },
          "slug": {
            "type": "string",
            "description": "URL-friendly identifier for the app",
            "example": "shifts_scheduling"
          },
          "name": {
            "type": "string",
            "description": "Display name of the core app",
            "example": "Shifts & Scheduling"
          },
          "description": {
            "type": "string",
            "description": "Description of the core app functionality",
            "example": "Complete shift scheduling and management system with team calendar"
          },
          "icon": {
            "type": "object",
            "description": "App icon information",
            "properties": {
              "url": {
                "type": "string",
                "description": "Icon class or URL",
                "example": "fas fa-calendar-alt"
              },
              "type": {
                "type": "string",
                "enum": [
                  "icon_class",
                  "url",
                  "default"
                ],
                "description": "Type of icon source. This is the authoritative, versioned contract —\nthe same three values on every app-listing endpoint:\n- icon_class: Font Awesome 5 class carried in `url`\n- url: HTTP(S) URL or asset path carried in `url`. Icons stored as\n  Active Storage attachments report as `url` too — where the server\n  keeps the image is not part of the contract.\n- default: built-in fallback asset path\nAdding a value is a BREAKING change (clients decode this as a strict\nenum), so it requires an API version bump. Server-side source of\ntruth: MobileViewSupport::API_ICON_TYPES.\n",
                "example": "icon_class"
              }
            }
          },
          "category": {
            "type": "string",
            "description": "App category (always 'core' for core apps)",
            "example": "core"
          },
          "version": {
            "type": "string",
            "description": "Current version of the app",
            "example": "1.0.0"
          },
          "color": {
            "type": "string",
            "description": "UI color theme for the app",
            "example": "primary"
          },
          "enabled": {
            "type": "boolean",
            "description": "Whether the core app is enabled for this business",
            "example": true
          },
          "features": {
            "type": "array",
            "description": "List of key features provided by this core app",
            "items": {
              "type": "string"
            },
            "example": [
              "Shift Creation",
              "Team Calendar",
              "Availability Management",
              "Schedule Templates"
            ]
          },
          "url": {
            "type": "string",
            "description": "Direct URL path to access the core app",
            "example": "/shifts"
          },
          "metadata": {
            "type": "object",
            "description": "Additional metadata about the core app",
            "properties": {
              "app_type": {
                "type": "string",
                "example": "core"
              },
              "always_available": {
                "type": "boolean",
                "example": true
              },
              "basic_workforce_feature": {
                "type": "boolean",
                "example": true
              },
              "advanced_feature": {
                "type": "boolean",
                "example": false
              }
            },
            "additionalProperties": true
          }
        },
        "required": [
          "id",
          "slug",
          "name",
          "category",
          "enabled",
          "url"
        ]
      },
      "ConsolidatedApp": {
        "type": "object",
        "description": "Unified app schema combining both core and marketplace apps",
        "properties": {
          "id": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "integer"
              }
            ],
            "description": "Unique identifier (string for core apps, integer for marketplace apps)",
            "example": "shifts_scheduling"
          },
          "slug": {
            "type": "string",
            "description": "URL-friendly identifier for the app",
            "example": "shifts_scheduling"
          },
          "name": {
            "type": "string",
            "description": "Display name of the app",
            "example": "Shifts & Scheduling"
          },
          "description": {
            "type": "string",
            "description": "Description of the app functionality",
            "example": "Complete shift scheduling and management system"
          },
          "type": {
            "type": "string",
            "enum": [
              "core",
              "marketplace"
            ],
            "description": "Type of app (core or marketplace)",
            "example": "core"
          },
          "icon": {
            "type": "object",
            "description": "App icon information. For mobile clients, always use the `url` field with `type: icon_class`.\nFont Awesome 5 icon classes are provided for all apps to ensure consistent mobile rendering.\n",
            "properties": {
              "url": {
                "type": "string",
                "description": "Font Awesome icon class (when type is \"icon_class\") or URL/path (when type is \"url\" or \"default\").\nMobile clients should always use this field for rendering icons.\n",
                "example": "fas fa-calendar-alt"
              },
              "type": {
                "type": "string",
                "enum": [
                  "icon_class",
                  "url",
                  "default"
                ],
                "description": "Type of icon source. This is the authoritative, versioned contract —\nthe same three values on every app-listing endpoint:\n- icon_class: Font Awesome 5 class (recommended for mobile)\n- url: Web URL or asset path. Icons stored as Active Storage\n  attachments report as `url` too — where the server keeps the image\n  is not part of the contract.\n- default: built-in fallback asset path\nAdding a value is a BREAKING change (clients decode this as a strict\nenum), so it requires an API version bump. Server-side source of\ntruth: MobileViewSupport::API_ICON_TYPES.\n",
                "example": "icon_class"
              },
              "web_url": {
                "type": "string",
                "nullable": true,
                "description": "Original SVG/image URL for web clients. Only present when the app has a custom SVG icon\nthat was converted to a Font Awesome class for mobile. Web clients can use this field\nto display the original custom icon if preferred.\n",
                "example": "/assets/icons/employee-performance-management.svg"
              }
            }
          },
          "category": {
            "type": "string",
            "description": "App category grouping (e.g., \"HR Management\", \"Workforce Management\")",
            "example": "Workforce Management"
          },
          "version": {
            "type": "string",
            "description": "Current version of the app",
            "example": "1.0.0"
          },
          "color": {
            "type": "string",
            "description": "UI color theme for the app",
            "example": "primary"
          },
          "enabled": {
            "type": "boolean",
            "description": "Whether the app is enabled for this business",
            "example": true
          },
          "featured": {
            "type": "boolean",
            "description": "Whether the app is featured (marketplace apps only)",
            "example": false
          },
          "sort_order": {
            "type": "integer",
            "description": "Display order for sorting apps",
            "example": 1
          },
          "url": {
            "type": "string",
            "description": "Direct URL path to access the app",
            "example": "/shifts"
          },
          "features": {
            "type": "array",
            "description": "List of key features provided by this app",
            "items": {
              "type": "string"
            },
            "example": [
              "Shift Creation",
              "Team Calendar",
              "Availability Management"
            ]
          },
          "configuration": {
            "type": "object",
            "description": "Business-specific configuration (marketplace apps only)",
            "additionalProperties": true,
            "example": {}
          },
          "enabled_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the app was enabled (marketplace apps only)",
            "example": "2024-01-15T10:30:00Z"
          },
          "metadata": {
            "type": "object",
            "description": "Additional metadata about the app",
            "additionalProperties": true,
            "example": {
              "app_type": "core",
              "always_available": true
            }
          },
          "pinned": {
            "type": "boolean",
            "description": "Whether this app is pinned for the calling user (mirrors the web\nsidebar's pinned list). **Only present when\n`include_navigation=true`** — `true` for every entry in\n`pinned_apps`, `false` for every entry in `apps`.\n",
            "example": true
          },
          "has_mobile_view": {
            "type": "boolean",
            "description": "Whether the app has a mobile-optimized (`/m/...`) view. Only present\nwhen `include_navigation=true`. Mobile clients never see an app with\n`false` here — those entries are filtered out of both lists.\n",
            "example": true
          },
          "mobile_url": {
            "type": "string",
            "nullable": true,
            "description": "The app's `/m/...` entry point, or `null` when it has no mobile view.\nOnly present when `include_navigation=true`.\n\nGotcha: on mobile, when an app's navigation collapses to a single\nchild page (see `navigation_items`), this is repointed at that child\npath and `navigation_items` comes back empty — so always prefer this\nvalue over deriving a path from `slug`.\n",
            "example": "/m/apps/ideas"
          },
          "unread_count": {
            "type": "integer",
            "description": "Unread badge count for the app tile. Opt-in via `?dashboard=true` or\nany `?include=` value containing `dashboard`, and emitted ONLY for\nthe apps that have an unread concept (`chat`, `news-feed`) and only\nwhen that entry is enabled. Counts come from the same source as\n`GET /api/v1/home`'s badges, so the two cannot drift. Best-effort:\nif a count query fails, the field is omitted for that app rather\nthan failing the request.\n",
            "example": 7
          },
          "unacknowledged_recognitions": {
            "type": "object",
            "description": "The recognitions the caller has RECEIVED and not yet seen — the\nconfetti feed. Present on the **`recognitions`** entry only, and only\nwhen that entry is enabled; every other app omits it.\n\nUnlike `unread_count` this is **NOT behind `?dashboard=true`**: a\nbadge a client forgot to ask for is a number it can fetch later, but\na celebration it never hears about is simply never shown. Always\npresent (as `{count: 0, items: []}`) once the node is there, so a\nclient can tell \"nothing to celebrate\" from \"this server predates the\nfeature\".\n\nBounded to the newest 20 received in the last 30 days, newest first\n— the client celebrates once, not once per item. Settle each item\nafterwards with `POST /api/v1/recognitions/{posts,awards}/{id}/acknowledge`\n(or by opening its detail screen, which acknowledges on its own), or\nthe next launch celebrates the same ones again. Best-effort: if the\nlookup fails the field is omitted rather than failing the app list.\n",
            "properties": {
              "count": {
                "type": "integer",
                "description": "How many items `items` carries (already capped at 20).",
                "example": 2
              },
              "items": {
                "type": "array",
                "maxItems": 20,
                "items": {
                  "type": "object",
                  "properties": {
                    "type": {
                      "type": "string",
                      "enum": [
                        "recognition_post",
                        "award"
                      ],
                      "description": "Which acknowledge path settles it — `recognition_post` → `/recognitions/posts/{id}/acknowledge`, `award` → `/recognitions/awards/{id}/acknowledge`.",
                      "example": "recognition_post"
                    },
                    "id": {
                      "type": "integer",
                      "example": 4471
                    },
                    "kind": {
                      "type": "string",
                      "enum": [
                        "recognition",
                        "award",
                        "certificate"
                      ],
                      "description": "The detail API's own vocabulary, so the client keys its copy off the same word on both surfaces. An automated Award (anniversary, milestone, lifecycle) is a `certificate`.",
                      "example": "recognition"
                    },
                    "title": {
                      "type": "string",
                      "nullable": true,
                      "description": "The award-card / award name, when there is one.",
                      "example": "Above and Beyond"
                    },
                    "from": {
                      "type": "string",
                      "nullable": true,
                      "description": "Who gave it, or `null` for an anonymous give — render \"someone\" from a `null`, never a name.",
                      "example": "Priya Raman"
                    },
                    "points": {
                      "type": "integer",
                      "example": 25
                    },
                    "received_at": {
                      "type": "string",
                      "format": "date-time",
                      "example": "2026-08-18T09:02:11Z"
                    }
                  }
                }
              }
            }
          },
          "navigation_items": {
            "type": "array",
            "description": "The app's in-app navigation (its web sidebar tabs / mobile TabBar),\nin display order. **Only present when `include_navigation=true`.**\n\n**Role-aware — absence is the authorization signal.** Items the\ncaller may not open are omitted, not disabled: e.g. Ideas'\n`review_queue` appears only for review-panel members (admins get no\nbypass) and its `campaigns` item disappears when the business turns\ncampaigns off; Forms' `approvals` appears only for reviewers. Do not\nrender an item the API didn't return.\n\nMay be an empty array: the app exposes no sub-navigation, every tab\nwas dropped for having no mobile route, the mobile single-child\ncollapse folded the only item into `mobile_url`, or the app's tab\nbuilder raised (failures degrade to `[]`, never a 500).\n",
            "items": {
              "$ref": "#/components/schemas/AppNavigationItem"
            }
          }
        },
        "required": [
          "id",
          "slug",
          "name",
          "type",
          "category",
          "enabled",
          "url"
        ]
      },
      "AppNavigationItem": {
        "type": "object",
        "description": "One entry of an app's in-app navigation, as returned in\n`ConsolidatedApp.navigation_items` by\n`GET /api/v1/apps?include_navigation=true`.\n\nSection headers and dividers from the web sidebar are stripped, and an\nitem with neither a `path` nor usable `actions` is dropped — so every\nentry you receive is something the user can actually open.\n",
        "properties": {
          "key": {
            "type": "string",
            "description": "Stable machine key for the item, safe to switch on client-side\n(titles are display copy and may be re-worded). Keys are per-app;\nIdeas emits `dashboard`, `all_ideas`, `campaigns`, `review_queue`;\nForms emits `my_submissions`, `approvals`; Wikis emits `dashboard`,\n`all_wikis`; Broadcast & Alerts emits `broadcast`, `alert`;\nCommunications emits `dashboard`, `feed`, `mail`,\n`my_posts`; Training emits `my_learning`, `catalog`, `my_records`,\n`my_team`; Recognitions emits `dashboard`, `feed`,\n`my_recognition`, `programs`, `awards`, `leaderboard`, `team` (note\n`awards` is the Award Cycles surface — the key matches the web\nsidebar's own tab key); Company Store emits `dashboard`, `catalog`,\n`orders`, `balance`, `approvals` (note `balance` is the Points\nsurface — again the web sidebar's own tab key). Apps whose navigation\ncomes from the generic sidebar builder use that app's own tab keys.\n",
            "example": "review_queue"
          },
          "title": {
            "type": "string",
            "description": "Display label, already localized/worded as the web sidebar shows it.",
            "example": "Reviews"
          },
          "icon": {
            "type": "string",
            "description": "Font Awesome class for the item (same convention as `ConsolidatedApp.icon.url`).",
            "example": "fas fa-scale-balanced"
          },
          "path": {
            "type": "string",
            "nullable": true,
            "description": "Path to open for this item. For web callers this is the desktop path\n(e.g. `/apps/ideas/list`); for mobile clients it is the validated\n`/m/...` equivalent — a tab whose desktop path has no real `/m/`\nroute is dropped from the array rather than returned with a dead\nlink. `null` only on a `native: true` item.\n",
            "example": "/apps/ideas/review"
          },
          "native": {
            "type": "boolean",
            "description": "Present (and always `true`) only for mobile callers on tabs the\niOS/Android client renders with its OWN native screen — there is no\nwebview route, so `path` is `null` and the client must dispatch on\n`key`. Applies to a fixed set of core-app tabs (Shifts'\n`my_shifts` / `my_availability`, Time & Attendance's\n`my_attendance`, Leave's `my_time_off`, Timesheets'\n`my_timesheets`). Absent on every other item.\n",
            "example": true
          },
          "count": {
            "type": "integer",
            "description": "Live badge count for the item. Only emitted where the builder\ncomputes one — today that is the Forms `approvals` item, whose count\nmatches exactly what `/forms/approvals` lists for this reviewer\n(whole-business `pending_review` for admin-tier, the manager's own\nreportees' `under_review` submissions for a manager). Treat as\noptional everywhere else.\n",
            "example": 3
          },
          "actions": {
            "type": "array",
            "description": "Secondary actions nested under the item (the sidebar's per-tab\ndropdown). Dividers/headers are stripped, and for mobile callers any\naction without a valid `/m/` route is removed — so this array can be\nshorter than the web menu, or absent when nothing survived.\n",
            "items": {
              "type": "object",
              "properties": {
                "title": {
                  "type": "string",
                  "example": "New idea"
                },
                "url": {
                  "type": "string",
                  "description": "Path to open, mobile-converted for mobile callers (same rule as `path`).",
                  "example": "/apps/ideas/new"
                },
                "icon": {
                  "type": "string",
                  "example": "fas fa-plus"
                }
              }
            }
          }
        }
      },
      "TrainingInfoRows": {
        "type": "array",
        "description": "The \"Course Info\" / \"Path Info\" card as ORDERED label/value pairs, in the order the web card renders them (Duration, Lessons, Difficulty, Credits, Certificate, Self-enroll), followed by the API-only Due and Version rows. The row set is tenant- and variant-dependent (an instructor-led course swaps the Duration/Lessons pair for Sessions available and adds Attendance), so a client renders what it is given rather than hardcoding fields. Blank values are omitted entirely; `Self-enroll` is always present and answers Yes / No / \"No (off in Training settings)\" — the EFFECTIVE state, matching the top-level `allow_self_enrollment` and the `cta`.",
        "items": {
          "type": "object",
          "required": [
            "label",
            "value"
          ],
          "properties": {
            "label": {
              "type": "string",
              "example": "Difficulty"
            },
            "value": {
              "type": "string",
              "example": "Intermediate"
            }
          }
        }
      },
      "TrainingReviewRow": {
        "type": "object",
        "description": "One approved review on a course/path detail.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 501
          },
          "rating": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5,
            "example": 5
          },
          "title": {
            "type": "string",
            "nullable": true,
            "example": "Clearest version I have seen"
          },
          "content": {
            "type": "string",
            "nullable": true,
            "example": "I printed page 2 and taped it inside the tool cabinet."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": "2026-07-24T09:00:00Z"
          },
          "mine": {
            "type": "boolean",
            "description": "The caller's own review.",
            "example": false
          },
          "reviewer": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer",
                "description": "The reviewer's user id.",
                "example": 41157
              },
              "name": {
                "type": "string",
                "example": "Priya Nair"
              },
              "initials": {
                "type": "string",
                "nullable": true,
                "example": "PN"
              },
              "job_title": {
                "type": "string",
                "nullable": true,
                "example": "Parts Counter"
              },
              "avatar_url": {
                "type": "string",
                "description": "ABSOLUTE avatar URL. Never null for a real person — a user with no profile photo gets a generated ui-avatars.com initials tile — so a client can render it unconditionally. `initials` is provided too, for a client that prefers to draw its own tile rather than fetch one.",
                "example": "https://ui-avatars.com/api/?name=Priya%20Nair&size=40&background=random"
              }
            }
          }
        }
      },
      "TrainingSessionRow": {
        "type": "object",
        "description": "One instructor-led session. Times are given BOTH in UTC and in the session's OWN timezone — the web renders every session in the venue's zone, never the viewer's, and a client must be able to do the same.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 90
          },
          "title": {
            "type": "string",
            "nullable": true,
            "example": "Morning cohort"
          },
          "session_type": {
            "type": "string",
            "enum": [
              "classroom",
              "webinar",
              "hybrid"
            ],
            "example": "classroom"
          },
          "starts_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "The instant, UTC. RENDER IT IN `timezone` — never the device's zone.",
            "example": "2026-08-20T15:00:00Z"
          },
          "ends_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": "2026-08-20T19:00:00Z"
          },
          "timezone": {
            "type": "string",
            "nullable": true,
            "example": "America/New_York"
          },
          "timezone_label": {
            "type": "string",
            "nullable": true,
            "description": "Short zone abbr, or UTC±offset.",
            "example": "EDT"
          },
          "duration_minutes": {
            "type": "integer",
            "nullable": true,
            "example": 240
          },
          "location": {
            "type": "string",
            "nullable": true,
            "example": "Training Room B"
          },
          "meeting_link": {
            "type": "string",
            "nullable": true,
            "example": null
          },
          "instructor": {
            "type": "object",
            "nullable": true,
            "properties": {
              "name": {
                "type": "string",
                "example": "Maria Lopez"
              },
              "job_title": {
                "type": "string",
                "nullable": true,
                "example": "Regional Safety Trainer"
              }
            }
          },
          "capacity": {
            "type": "object",
            "properties": {
              "limit": {
                "type": "integer",
                "nullable": true,
                "example": 12
              },
              "taken": {
                "type": "integer",
                "example": 9
              },
              "seats_available": {
                "type": "integer",
                "nullable": true,
                "example": 3
              },
              "full": {
                "type": "boolean",
                "example": false
              },
              "waitlist_count": {
                "type": "integer",
                "example": 0
              }
            }
          },
          "registration": {
            "type": "object",
            "properties": {
              "open": {
                "type": "boolean",
                "description": "Scheduled and the deadline has not passed.",
                "example": true
              },
              "closes_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "example": "2026-08-19T15:00:00Z"
              }
            }
          },
          "my_registration": {
            "type": "object",
            "nullable": true,
            "description": "Present only when the caller's seat is on THIS session.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 771
              },
              "status": {
                "type": "string",
                "example": "registered"
              },
              "waitlisted": {
                "type": "boolean",
                "example": false
              },
              "waitlist_position": {
                "type": "integer",
                "nullable": true,
                "example": null
              }
            }
          },
          "cta_action": {
            "type": "string",
            "description": "The web row's precedence: a seat you already hold wins, then a closed registration, then a full session (waitlist), then switching, then booking. `switch` appears only when the caller's seat is CHANGEABLE (registered or waitlisted) — a session already attended or completed is never offered as something to switch away from, since that would discard earned attendance.",
            "enum": [
              "registered",
              "waitlisted",
              "closed",
              "join_waitlist",
              "switch",
              "register"
            ],
            "example": "register"
          }
        }
      },
      "TrainingLessonRow": {
        "type": "object",
        "description": "One row of a course's lesson outline. Byte-identical whether it arrives inlined on the course detail or from the Lessons tab endpoint — both serialize the same outline through one serializer, so the tab can never disagree with the detail it was opened from.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 91
          },
          "title": {
            "type": "string",
            "example": "Lockout/Tagout Procedure"
          },
          "content_type": {
            "type": "string",
            "enum": [
              "text",
              "video",
              "document",
              "quiz",
              "scorm",
              "partner_course"
            ],
            "example": "video"
          },
          "type_label": {
            "type": "string",
            "description": "From the canonical TrainingLesson#content_type_label, so the app names a lesson exactly as the web page does (e.g. \"SCORM\", not \"Scorm\").",
            "example": "Video"
          },
          "provider": {
            "type": "string",
            "nullable": true,
            "description": "The vendor behind a `partner_course` lesson — the human-readable name the web and `/m/` lesson bodies print above the launch card (e.g. \"Go1\"), so a client can say WHOSE course was taken rather than only that it was a partner's. Null for every other `content_type`.",
            "example": null
          },
          "type_icon": {
            "type": "string",
            "example": "fa-video"
          },
          "duration_minutes": {
            "type": "integer",
            "nullable": true,
            "description": "The AUTHOR'S ESTIMATE, in minutes. For a video this is not necessarily the video's length — see `meta_line`.",
            "example": 12
          },
          "meta_line": {
            "type": "string",
            "nullable": true,
            "description": "The lesson's one-line label, composed exactly as the web player's eyebrow and the `/m/` reader render it — \"Text lesson · 5 min read\", \"Video lesson · 0:48 watch\", \"PDF document · 10 min\", \"SCORM module\". RENDER THIS instead of composing your own from `type_label` + `duration_minutes`: a native client drawing its own lesson header is trying to match the other two surfaces, and those two fields cannot reproduce them. `type_label` is the bare type (\"Video\", not \"Video lesson\"), and `duration_minutes` is the author's estimate while an uploaded video's REAL length is measured from the file — lesson 479 renders \"Video lesson · 0:48 watch\" where the raw fields compose to \"Video · ~8 min\".\n\nTHE DURATION RULE IS PER TYPE and is deliberately server-side, because it is the part that has drifted: ffprobed-length-then-estimate suffixed \"watch\" for video, the estimate suffixed \"read\" for text, bare minutes for a document, and NO duration for `scorm` / `quiz` / `partner_course` — those render a launch card whose estimated time is a tile inside the card, not part of this line.\n\nThe raw fields stay for clients that compose their own. Null only when the lesson's type and duration are both unresolvable.",
            "example": "Video lesson · 0:48 watch"
          },
          "required": {
            "type": "boolean",
            "example": true
          },
          "completed": {
            "type": "boolean",
            "example": false
          },
          "current": {
            "type": "boolean",
            "description": "The lesson the CTA resumes into. At most one row carries this, and it is always the same lesson as the detail's `cta.lesson_id`, so a client can open the player from either. Never true for a caller with no enrollment.",
            "example": true
          },
          "locked": {
            "type": "boolean",
            "description": "True only when the caller holds no enrollment — lessons are not sequentially gated within a course.",
            "example": false
          },
          "web_view_url": {
            "type": "string",
            "nullable": true,
            "description": "Absolute URL of the BARE mobile render of this lesson (`/m/apps/training/courses/:course_id/lessons/:id`) — the whole ONLINE story for taking a self-paced lesson: load it in a native WebView and the lesson displays with no header, no bottom tab bar, no prev/next and no Mark Complete, because the native screen draws its own and calls `POST …/lessons/{lesson_id}/progress` for the completion.\n\nALWAYS CARRIES `?embed=1`, and that param is the ONLY thing that strips the chrome — load this url VERBATIM. Unlike the Wikis reader there is no User-Agent fallback here, deliberately: the existing `/m/` Training pages must keep their chrome for anyone browsing them in a mobile browser, so the bare layout is strictly opt-in. Drop the param and you get the full mobile page inside your native screen.\n\nRenders every self-paced type: text, video (uploaded or embedded) and document inline; a `scorm` or `partner_course` lesson shows a launch card whose button opens the provider's runtime full-screen and returns here on exit. Null when the serializer had no request context.\n\nA COURSEWARE LESSON HAS A NATIVE CARD TOO — `GET …/lessons/{lesson_id}/scorm` — and it is the sibling of the quiz card described below, not a lesser surface. It returns the module's status, score, resume sentence/percent, bookmark, launch count, package standard, mastery score, time limit and a resolved `cta`, so a client can draw the whole Status / Score / Estimated time / Resume panel natively; `POST …/lessons/{lesson_id}/scorm/restart` is the \"Start over\". Only the SCORM/xAPI/cmi5/AICC RUNTIME itself has to stay a WebView (inherent to those standards) — the card around it does not. This paragraph exists because it was missing: the quiz pointer below was here and this one was not, so ISS-20260910-827-69F8A8 was filed asking for courseware status to be added to this very row by an engineer who read the row serializer, found nothing, and never saw the dedicated endpoint next door. Do NOT expect these fields on the row — they are one lesson-addressed call away, deliberately, for the same reason the quiz card is not inlined here.\n\nA `quiz` LESSON SHOWS A QUIZ CARD, AND ITS BUTTON IS AN INTERCEPTION POINT. The card renders the question count, passing bar, attempt tallies and the learner's standing result, and its Start/Retake button is a plain GET link to `/m/apps/training/courses/{course_id}/lessons/{lesson_id}/quiz/launch`. A native client that wants to run the assessment itself should MATCH THAT PATH in its WebView, cancel the navigation, and drive the quiz endpoints instead — `GET …/lessons/{lesson_id}/quiz` for the same card state, then `POST …/quiz/attempts` and the rest. Reload this url when the native screen closes: passing writes the lesson completion and moves course progress, so the card underneath is stale until you do.\n\nA client that does NOT intercept needs no special handling — the link is a real route that starts or resumes the attempt and lands the learner in the responsive web player, so leaving it alone is a working flow rather than a dead button. It is a GET for exactly this reason: a POST form is not reliably visible to Android's `shouldOverrideUrlLoading`.",
            "example": "https://officechat.workforce.mangoapps.com/m/apps/training/courses/37/lessons/91?embed=1"
          }
        }
      },
      "TrainingPathStepRow": {
        "type": "object",
        "description": "One step of a learning path with its member courses. Identical whether it arrives inlined on the path detail or from the Steps tab endpoint.\n\n`courses` is ordered by the author's `sort_order` with the join row's id breaking ties: two member courses may legitimately share a sort_order, and without the tiebreak the same step came back in different orders depending on how the association had been preloaded.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 12
          },
          "position": {
            "type": "integer",
            "description": "1-based.",
            "example": 2
          },
          "name": {
            "type": "string",
            "nullable": true,
            "example": "Core compliance"
          },
          "courses_count": {
            "type": "integer",
            "example": 3
          },
          "rule": {
            "type": "string",
            "description": "The step's completion rule chip.",
            "example": "All required + 1 of the optional"
          },
          "complete": {
            "type": "boolean",
            "example": false
          },
          "locked": {
            "type": "boolean",
            "description": "Sequential path AND enrolled caller AND after the first incomplete step — all three, so a learner browsing a path they have not joined never sees a padlock.",
            "example": false
          },
          "completion_percent": {
            "type": "integer",
            "description": "0..100 fill for the step's pill — the fraction of the step's REQUIRED weight satisfied. Counts COURSES not lessons",
            "so it is instructor-led-safe.": null,
            "example": 67
          },
          "unlocks_after_step": {
            "type": "integer",
            "nullable": true,
            "description": "Present only when locked — the 1-based step that must finish first.",
            "example": null
          },
          "courses": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 60
                },
                "title": {
                  "type": "string",
                  "example": "Benefits & Policies"
                },
                "required": {
                  "type": "boolean",
                  "example": true
                },
                "delivery_label": {
                  "type": "string",
                  "enum": [
                    "Self-paced",
                    "Instructor-Led",
                    "Learning Path"
                  ],
                  "example": "Self-paced"
                },
                "progress_percentage": {
                  "type": "integer",
                  "example": 100
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "not_started",
                    "in_progress",
                    "completed"
                  ],
                  "example": "completed"
                },
                "cta_action": {
                  "type": "string",
                  "enum": [
                    "locked",
                    "review",
                    "resume",
                    "start",
                    "enroll_required"
                  ],
                  "example": "review"
                }
              }
            }
          }
        }
      },
      "TrainingSubjectBlock": {
        "type": "object",
        "description": "The course or learning path a secondary screen belongs to — a reviews or Q&A list, a Lessons or Steps tab — so the client can title the screen without a second call. Every one of those endpoints emits it from the one helper (Api::V1::Training::SubjectPayload), the same one behind the `subject` block on the My Training cards.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 37
          },
          "title": {
            "type": "string",
            "example": "Workplace Safety Fundamentals"
          },
          "type": {
            "type": "string",
            "enum": [
              "course",
              "path"
            ],
            "example": "course"
          },
          "delivery_label": {
            "type": "string",
            "enum": [
              "Self-paced",
              "Instructor-Led",
              "Learning Path"
            ],
            "example": "Self-paced"
          },
          "category": {
            "type": "object",
            "nullable": true,
            "properties": {
              "name": {
                "type": "string",
                "example": "Compliance"
              },
              "color": {
                "type": "string",
                "nullable": true,
                "example": null
              },
              "icon": {
                "type": "string",
                "nullable": true,
                "example": null
              }
            }
          }
        }
      },
      "TrainingAnswerRow": {
        "type": "object",
        "description": "One answer inside a Q&A thread. `instructor` drives the INSTRUCTOR badge and is true only for a user holding the COURSE's Instructor role (directly or through a group) — a learning path has no per-path role, so it is always false there, matching the web badge exactly.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 23
          },
          "body": {
            "type": "string",
            "example": "Yes. There is no time threshold — if a guard comes off, the machine gets locked out."
          },
          "votes_count": {
            "type": "integer",
            "description": "Upvotes (\"helpful\") on this answer.",
            "example": 12
          },
          "is_best_answer": {
            "type": "boolean",
            "description": "At most one per question.",
            "example": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": "2026-07-15T05:54:02Z"
          },
          "mine": {
            "type": "boolean",
            "description": "The caller wrote this answer.",
            "example": false
          },
          "my_vote": {
            "type": "boolean",
            "description": "The caller has upvoted it.",
            "example": false
          },
          "author": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer",
                "description": "The answerer's user id.",
                "example": 1
              },
              "name": {
                "type": "string",
                "example": "Maria Lopez"
              },
              "initials": {
                "type": "string",
                "nullable": true,
                "example": "ML"
              },
              "job_title": {
                "type": "string",
                "nullable": true,
                "example": "Regional Safety Trainer"
              },
              "avatar_url": {
                "type": "string",
                "description": "ABSOLUTE avatar URL. Never null for a real person — a user with no profile photo gets a generated ui-avatars.com initials tile — so a client can render it unconditionally. `initials` is provided too, for a client that prefers to draw its own tile rather than fetch one.",
                "example": "https://ui-avatars.com/api/?name=Priya%20Nair&size=40&background=random"
              },
              "instructor": {
                "type": "boolean",
                "example": true
              }
            }
          }
        }
      },
      "TrainingQuestionRow": {
        "type": "object",
        "description": "One question thread — the question plus its answers, mirroring one card of the web Q&A tab.\n`status` is what the client renders the waiting state from: an `unanswered` question shows the \"waiting on your instructor\" strip instead of an answer block.\n`best_answer` is a CONVENIENCE POINTER, not a second copy — it is the same object already present in `answers` (which is ordered best-answer first, then most upvoted, the web's exact sort), so a client with room for one answer reads it directly. It is null until someone marks a best answer, and a question can be `answered` with no best answer.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 30
          },
          "body": {
            "type": "string",
            "example": "Do I need lockout/tagout for a two-minute belt change?"
          },
          "status": {
            "type": "string",
            "enum": [
              "unanswered",
              "answered"
            ],
            "example": "answered"
          },
          "votes_count": {
            "type": "integer",
            "description": "Upvotes (\"helpful\") on the question.",
            "example": 17
          },
          "answers_count": {
            "type": "integer",
            "example": 2
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": "2026-07-15T05:54:02Z"
          },
          "mine": {
            "type": "boolean",
            "description": "The caller asked this.",
            "example": true
          },
          "my_vote": {
            "type": "boolean",
            "description": "The caller has upvoted it.",
            "example": false
          },
          "author": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer",
                "description": "The asker's user id.",
                "example": 41157
              },
              "name": {
                "type": "string",
                "example": "Priya Nair"
              },
              "initials": {
                "type": "string",
                "nullable": true,
                "example": "PN"
              },
              "job_title": {
                "type": "string",
                "nullable": true,
                "example": "Parts Counter"
              },
              "avatar_url": {
                "type": "string",
                "description": "ABSOLUTE avatar URL. Never null for a real person — a user with no profile photo gets a generated ui-avatars.com initials tile — so a client can render it unconditionally. `initials` is provided too, for a client that prefers to draw its own tile rather than fetch one.",
                "example": "https://ui-avatars.com/api/?name=Priya%20Nair&size=40&background=random"
              }
            }
          },
          "best_answer": {
            "allOf": [
              {
                "$ref": "#/components/schemas/TrainingAnswerRow"
              }
            ],
            "nullable": true
          },
          "answers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TrainingAnswerRow"
            }
          }
        }
      },
      "TrainingFeedbackPageMeta": {
        "type": "object",
        "description": "Pagination over the list (build_pagination_meta).",
        "properties": {
          "total_count": {
            "type": "integer",
            "example": 25
          },
          "total_pages": {
            "type": "integer",
            "example": 2
          },
          "current_page": {
            "type": "integer",
            "example": 1
          },
          "per_page": {
            "type": "integer",
            "example": 20
          },
          "has_next_page": {
            "type": "boolean",
            "example": true
          },
          "has_prev_page": {
            "type": "boolean",
            "example": false
          }
        }
      },
      "TrainingReviewsPage": {
        "type": "object",
        "description": "The full Ratings & Reviews list for a course or a learning path — one payload, served by both nested routes.",
        "required": [
          "subject",
          "rating",
          "reviews",
          "meta"
        ],
        "properties": {
          "subject": {
            "$ref": "#/components/schemas/TrainingSubjectBlock"
          },
          "rating": {
            "type": "object",
            "description": "The histogram header. APPROVED reviews only, and identical to the `rating` block on the course/path detail (both read HasReviews).",
            "properties": {
              "average": {
                "type": "number",
                "nullable": true,
                "description": "null when there are no approved reviews.",
                "example": 3.5
              },
              "count": {
                "type": "integer",
                "example": 2
              },
              "distribution": {
                "type": "object",
                "description": "Star -> count, for the 5..1 histogram. SPARSE — a star with no reviews is ABSENT, not zero, so treat a missing key as 0. (Same shape the detail endpoints return.)",
                "additionalProperties": {
                  "type": "integer"
                },
                "example": {
                  "5": 1,
                  "2": 1
                }
              }
            }
          },
          "active_sort": {
            "type": "string",
            "description": "The sort actually applied — an unrecognised `sort` falls back to `recent`. `helpful` is deliberately NOT a sort here (the Helpful affordance is admin-only on the web), so `sort=helpful` is one of the unrecognised values and comes back as `recent`.",
            "enum": [
              "recent",
              "lowest"
            ],
            "example": "recent"
          },
          "reviews": {
            "type": "array",
            "description": "The requested page. The caller's OWN review is included here as well as in `my_review` (matching the detail endpoints); `mine` flags it.",
            "items": {
              "$ref": "#/components/schemas/TrainingReviewRow"
            }
          },
          "my_review": {
            "allOf": [
              {
                "$ref": "#/components/schemas/TrainingReviewRow"
              }
            ],
            "nullable": true,
            "description": "The caller's own review whether or not it falls on this page — the sheet's CTA reads \"Edit your review\" vs \"Rate this course\" off it. Null when they have not reviewed. Unlike `reviews`, this is NOT filtered to approved, so an author still sees a review awaiting moderation."
          },
          "can_review": {
            "type": "boolean",
            "description": "Enrolled AND has not already reviewed (TrainingReview.user_can_review? — the same rule the web gate applies). False once `my_review` is set.",
            "example": true
          },
          "meta": {
            "$ref": "#/components/schemas/TrainingFeedbackPageMeta"
          }
        }
      },
      "TrainingQuestionsPage": {
        "type": "object",
        "description": "The Q&A thread list for a course or a learning path — one payload, served by both nested routes.",
        "required": [
          "subject",
          "counts",
          "questions",
          "meta"
        ],
        "properties": {
          "subject": {
            "$ref": "#/components/schemas/TrainingSubjectBlock"
          },
          "active_filter": {
            "type": "string",
            "description": "The filter actually applied — an unrecognised `filter` falls back to `all`.",
            "enum": [
              "all",
              "unanswered",
              "top"
            ],
            "example": "all"
          },
          "counts": {
            "type": "object",
            "description": "FILTER-INDEPENDENT tallies, so all three pills can be badged from one request. `answered` is derived (`total - unanswered`), because the two statuses partition the set.",
            "properties": {
              "total": {
                "type": "integer",
                "example": 25
              },
              "answered": {
                "type": "integer",
                "example": 9
              },
              "unanswered": {
                "type": "integer",
                "example": 16
              }
            }
          },
          "questions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TrainingQuestionRow"
            }
          },
          "permissions": {
            "type": "object",
            "description": "What this caller may do from the screen. Both are true for anyone who got a 200 — asking and answering carry no per-owner gate on the web either, so app access is the whole rule. Marking a best answer is content-manager-only and has no mobile surface, so it is deliberately not reported.",
            "properties": {
              "can_ask": {
                "type": "boolean",
                "example": true
              },
              "can_answer": {
                "type": "boolean",
                "example": true
              },
              "can_mark_best": {
                "type": "boolean",
                "description": "May the caller mark a best answer on this owner — the ONE Q&A action with a per-owner permission (a Training admin, the COURSE's Instructor, or a learning path's creator). Render the Mark-best affordance from THIS flag: `POST /training/qa/answers/{id}/mark_best` exists for every caller and answers 403 `forbidden` for a learner, so keying the button on the endpoint's existence shows it to everyone.",
                "example": false
              }
            }
          },
          "meta": {
            "$ref": "#/components/schemas/TrainingFeedbackPageMeta"
          }
        }
      },
      "TrainingTeamStats": {
        "type": "object",
        "description": "A team's headline metrics, counted off ONE relation so they cannot disagree with each other. `.current` enrollments only, so a learner who retook a course is counted once.",
        "properties": {
          "team_size": {
            "type": "integer",
            "description": "Headcount in scope (the TRUE total",
            "not the page size).": null,
            "example": 142
          },
          "total_enrollments": {
            "type": "integer",
            "example": 119
          },
          "completed": {
            "type": "integer",
            "example": 39
          },
          "in_progress": {
            "type": "integer",
            "example": 31
          },
          "overdue": {
            "type": "integer",
            "example": 17
          },
          "not_started": {
            "type": "integer",
            "example": 48
          },
          "completion_rate": {
            "type": "number",
            "description": "Percent, 1dp. 0 (never null) for a team with no enrollments.",
            "example": 32.8
          },
          "avg_progress": {
            "type": "number",
            "description": "Mean progress percentage across current enrollments, 1dp. A NUMBER, not a string — Postgres AVG returns a BigDecimal and is cast before encoding.",
            "example": 42.6
          }
        }
      },
      "TrainingTeamMemberRow": {
        "type": "object",
        "description": "One learner card on the roster. Counts ship raw so the client owns the presentation: the progress bar is `done / total`, the caption is \"N of M complete\", and both the bar colour and the card's alert border key off `overdue > 0`.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 41157
          },
          "name": {
            "type": "string",
            "example": "Priya Raman"
          },
          "initials": {
            "type": "string",
            "nullable": true,
            "example": "PR"
          },
          "title": {
            "type": "string",
            "nullable": true,
            "description": "Job title in this business.",
            "example": "Service Advisor"
          },
          "avatar_url": {
            "type": "string",
            "description": "ABSOLUTE avatar URL; never null (falls back to a generated initials tile).",
            "example": "https://ui-avatars.com/api/?name=Priya%20Raman&size=40&background=random"
          },
          "total": {
            "type": "integer",
            "description": "Current enrollments.",
            "example": 7
          },
          "done": {
            "type": "integer",
            "example": 5
          },
          "overdue": {
            "type": "integer",
            "example": 2
          }
        }
      },
      "TrainingTeamEnrollmentRow": {
        "type": "object",
        "description": "One enrollment row inside My Team. Raw values, no pre-composed sentences — the client builds \"Due 12 Aug · 40% done\" / \"3 days overdue\" itself, so the copy stays translatable.\n`learner` is present only where the row appears OUT of a learner's context (the dashboard's overdue list, which spans the team); the member drill-in omits it because its header already names the person.",
        "properties": {
          "enrollment_id": {
            "type": "integer",
            "description": "Pass this to the remind endpoint.",
            "example": 22989
          },
          "course_id": {
            "type": "integer",
            "nullable": true,
            "example": 128
          },
          "title": {
            "type": "string",
            "nullable": true,
            "example": "Forklift Operator Certification"
          },
          "status": {
            "type": "string",
            "example": "enrolled"
          },
          "progress": {
            "type": "integer",
            "description": "Percent complete",
            "0..100.": null,
            "example": 40
          },
          "overdue": {
            "type": "boolean",
            "example": true
          },
          "days_overdue": {
            "type": "integer",
            "nullable": true,
            "description": "null when not overdue — 0 would read as \"due today\".",
            "example": 25
          },
          "due_date": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": "2026-08-06T00:00:00Z"
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": null
          },
          "learner": {
            "type": "object",
            "nullable": true,
            "description": "Present on the dashboard's overdue rows only.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 1257
              },
              "name": {
                "type": "string",
                "example": "Melanie Kirkwood"
              },
              "initials": {
                "type": "string",
                "nullable": true,
                "example": "MK"
              },
              "title": {
                "type": "string",
                "nullable": true,
                "example": "Shift Lead"
              },
              "avatar_url": {
                "type": "string",
                "example": "https://ui-avatars.com/api/?name=Melanie%20Kirkwood&size=40&background=random"
              }
            }
          }
        }
      },
      "TrainingMyTeamDashboard": {
        "type": "object",
        "description": "The My Team dashboard for ONE tab. `members`, `overdue` and `completions` always all appear and only the tab named by `active_tab` carries rows — stable key types for a generated client, at the cost of two empty arrays. `stats` and `pills` are tab-independent; `meta` paginates the ACTIVE tab.",
        "required": [
          "scope",
          "active_tab",
          "stats",
          "pills",
          "members",
          "overdue",
          "completions",
          "meta"
        ],
        "properties": {
          "active_tab": {
            "type": "string",
            "description": "The tab actually applied — an unrecognised `tab` falls back to `people`. Tells the client which of the three list keys to read.",
            "enum": [
              "people",
              "overdue",
              "done"
            ],
            "example": "people"
          },
          "scope": {
            "type": "object",
            "description": "Who the caller is looking at. `total` and `per_page` ship separately from the page so the client can compose its own \"Showing N of M\" caption rather than parse a sentence, and `admin_view` tells it whether to say \"Employees\" or \"Team\". `key` + `available` drive the Team Scope segmented control: render it only when `available` holds more than one value, and select `key`.",
            "properties": {
              "key": {
                "type": "string",
                "description": "The scope actually applied. An admin defaults to `all_employees` and may narrow to `direct_reports`; a manager is always `direct_reports`.",
                "enum": [
                  "direct_reports",
                  "all_employees"
                ],
                "example": "all_employees"
              },
              "available": {
                "type": "array",
                "description": "What THIS caller may request via `?scope=`. Both values for a Training admin, `[\"direct_reports\"]` alone for a people-manager — the role is a ceiling, so a manager requesting `all_employees` is still served their own reports.",
                "items": {
                  "type": "string",
                  "enum": [
                    "direct_reports",
                    "all_employees"
                  ]
                },
                "example": [
                  "direct_reports",
                  "all_employees"
                ]
              },
              "admin_view": {
                "type": "boolean",
                "description": "true = these rows are every active member of the business. Derived from the APPLIED scope rather than the role, so an admin who has narrowed to `direct_reports` reads false and labels the roster \"Team\".",
                "example": true
              },
              "total": {
                "type": "integer",
                "example": 142
              },
              "per_page": {
                "type": "integer",
                "example": 20
              },
              "capped": {
                "type": "boolean",
                "description": "true when the roster is larger than one page.",
                "example": true
              }
            }
          },
          "stats": {
            "$ref": "#/components/schemas/TrainingTeamStats"
          },
          "pills": {
            "type": "array",
            "description": "The three filter pills with the counts that badge them. Keys are stable (`people`, `overdue`, `done`).",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string",
                  "enum": [
                    "people",
                    "overdue",
                    "done"
                  ],
                  "example": "overdue"
                },
                "count": {
                  "type": "integer",
                  "example": 17
                }
              }
            }
          },
          "members": {
            "type": "array",
            "description": "The roster page, ordered by first then last name. Populated only when `active_tab` is `people`; otherwise an empty array.",
            "items": {
              "$ref": "#/components/schemas/TrainingTeamMemberRow"
            }
          },
          "overdue": {
            "type": "array",
            "description": "Overdue rows across the WHOLE team (not just the roster page — the pill counts the team, so its list must too), soonest-due first and PAGINATED. Each row names its learner. Populated only when `active_tab` is `overdue`.",
            "items": {
              "$ref": "#/components/schemas/TrainingTeamEnrollmentRow"
            }
          },
          "completions": {
            "type": "array",
            "description": "Most recent completions across the whole team, PAGINATED. Populated only when `active_tab` is `done`.",
            "items": {
              "$ref": "#/components/schemas/TrainingTeamEnrollmentRow"
            }
          },
          "meta": {
            "allOf": [
              {
                "$ref": "#/components/schemas/TrainingFeedbackPageMeta"
              }
            ],
            "description": "Pagination for the ACTIVE tab — so `total_count` is the roster size on `people`, the overdue count on `overdue`, and the completed count on `done`."
          }
        }
      },
      "TrainingMyTeamMember": {
        "type": "object",
        "required": [
          "member",
          "counts",
          "open",
          "completed",
          "certificates"
        ],
        "properties": {
          "member": {
            "$ref": "#/components/schemas/TrainingTeamMemberRow"
          },
          "counts": {
            "type": "object",
            "description": "`assigned` vs `in_progress` splits on PROGRESS, not status: a learner who opened a course but finished nothing counts as assigned.",
            "properties": {
              "assigned": {
                "type": "integer",
                "example": 2
              },
              "in_progress": {
                "type": "integer",
                "example": 3
              },
              "completed": {
                "type": "integer",
                "example": 5
              },
              "overdue": {
                "type": "integer",
                "example": 1
              }
            }
          },
          "open": {
            "type": "array",
            "description": "Everything outstanding, newest first. `learner` is omitted (the header names them).",
            "items": {
              "$ref": "#/components/schemas/TrainingTeamEnrollmentRow"
            }
          },
          "completed": {
            "type": "array",
            "description": "The completed history, newest first.",
            "items": {
              "$ref": "#/components/schemas/TrainingTeamEnrollmentRow"
            }
          },
          "certificates": {
            "type": "array",
            "description": "Issued certificates, newest first, capped at 12.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 64
                },
                "title": {
                  "type": "string",
                  "nullable": true,
                  "example": "Workplace Safety Fundamentals"
                },
                "issued_at": {
                  "type": "string",
                  "format": "date-time",
                  "nullable": true,
                  "example": "2026-07-24T09:00:00Z"
                }
              }
            }
          }
        }
      },
      "TrainingReviewCreateRequest": {
        "type": "object",
        "description": "Body of a review POST (a course or a learning path — same shape for both).\nEvery field must be a SINGLE SCALAR. A collection shape (`rating[]=5`, `title[a]=x`) is read as ABSENT rather than coerced — so a non-scalar `rating` fails its presence validation into the normal per-field 422, and a non-scalar `title`/`content` is stored as null instead of as the collection's own string form.",
        "required": [
          "rating"
        ],
        "properties": {
          "rating": {
            "type": "integer",
            "description": "Whole stars, 1..5. Missing or out of range answers 422 with a per-field error.",
            "minimum": 1,
            "maximum": 5,
            "example": 5
          },
          "title": {
            "type": "string",
            "nullable": true,
            "maxLength": 255,
            "example": "Genuinely useful"
          },
          "content": {
            "type": "string",
            "nullable": true,
            "maxLength": 5000,
            "example": "The lockout/tagout walkthrough matched our floor exactly."
          }
        }
      },
      "TrainingReviewUpdateRequest": {
        "type": "object",
        "description": "Body of a review PATCH. Every field is OPTIONAL and applied only when the key is PRESENT, so a client can change the stars without resending prose it is not touching — omitting `title` leaves the stored title alone, whereas sending it blank clears it.\nAt least one of the three must be present; an empty body is 422 `nothing_to_update` rather than a silent no-op.\nSame single-scalar rule as the create request: a collection shape is read as ABSENT, so `title[]=x` clears the title rather than storing the collection's own string form.",
        "minProperties": 1,
        "properties": {
          "rating": {
            "type": "integer",
            "description": "Whole stars, 1..5. Sending it blank or out of range is 422 with a per-field error — it does NOT fall back to the stored value.",
            "minimum": 1,
            "maximum": 5,
            "example": 4
          },
          "title": {
            "type": "string",
            "nullable": true,
            "maxLength": 255,
            "description": "Send blank to clear.",
            "example": "Better on a second pass"
          },
          "content": {
            "type": "string",
            "nullable": true,
            "maxLength": 5000,
            "description": "Send blank to clear.",
            "example": "Revisited after the refresher — the LOTO section is the strongest part."
          }
        }
      },
      "TrainingReviewWriteResult": {
        "type": "object",
        "description": "The payload of EITHER review write — the 201 from a POST or the 200 from a PATCH. One shape, because a client refreshes the same summary block after either. It carries the RECOMPUTED `rating` histogram (never the pre-write one), which is the whole reason to read this response instead of patching a cached row: editing the stars moves the histogram.\n(Named `TrainingReviewCreated` until the edit endpoint shipped; renamed because the shape was never create-specific.)",
        "required": [
          "subject",
          "rating",
          "review",
          "can_review"
        ],
        "properties": {
          "subject": {
            "$ref": "#/components/schemas/TrainingSubjectBlock"
          },
          "rating": {
            "type": "object",
            "description": "The histogram header including the review just posted — identical in shape to the `rating` block on the reviews list and the course/path detail (all three read HasReviews#rating_summary).",
            "properties": {
              "average": {
                "type": "number",
                "nullable": true,
                "example": 4.7
              },
              "count": {
                "type": "integer",
                "example": 3
              },
              "distribution": {
                "type": "object",
                "description": "Star -> count. SPARSE — treat a missing key as 0.",
                "additionalProperties": {
                  "type": "integer"
                },
                "example": {
                  "5": 2,
                  "4": 1
                }
              }
            }
          },
          "review": {
            "$ref": "#/components/schemas/TrainingReviewRow"
          },
          "can_review": {
            "type": "boolean",
            "description": "Always false here — the caller has just used their one review. Same key the reviews GET returns, so the CTA flips to \"Edit your review\" off this response.",
            "example": false
          }
        }
      },
      "TrainingQaPostRequest": {
        "type": "object",
        "description": "Body of the Q&A POST. `question_id` is what selects between the endpoint's two behaviours: absent asks a new question, present posts an answer (a \"reply\") to that question.",
        "required": [
          "body"
        ],
        "properties": {
          "body": {
            "type": "string",
            "description": "The question or answer text. Must be a SINGLE SCALAR — whitespace-only AND any collection shape (`body[]=x`, `body[a]=x`) are rejected 422 `body_required`, so a malformed body can never be stored as the collection's own string form. Capped at 2000 characters for a question and 5000 for an answer (model validations — an over-long body answers 422 with a per-field error).",
            "example": "Do I need lockout/tagout for a two-minute belt change?"
          },
          "question_id": {
            "type": "integer",
            "nullable": true,
            "description": "Omit to ASK. Pass a question id to REPLY to it. Must name a question on THIS course / learning path — anything else is 422 `question_not_found`, never a misfiled answer. There is no reply-to-a-reply: an answer has no parent answer.\nMust be a SINGLE SCALAR. A collection shape (`question_id[]=30`) is a malformed reply TARGET and answers 422 `question_not_found` — it does NOT degrade into asking a new question, which would file the text somewhere the caller never asked for.",
            "example": 30
          }
        }
      },
      "TrainingQaThread": {
        "type": "object",
        "description": "ONE Q&A thread and its context — the payload of every Q&A WRITE (the ask/reply POST and mark_best), so a client patches its card the same way whatever it just did. `question` is the exact row shape the Q&A list returns, re-read after the write, so the thread already reflects the new answer, the re-sorted `answers` and any `status` change.",
        "required": [
          "subject",
          "question",
          "counts",
          "permissions"
        ],
        "properties": {
          "subject": {
            "$ref": "#/components/schemas/TrainingSubjectBlock"
          },
          "question": {
            "$ref": "#/components/schemas/TrainingQuestionRow"
          },
          "answer_id": {
            "type": "integer",
            "nullable": true,
            "description": "The answer row this call created or marked, so a client can highlight it without diffing. Null when a new QUESTION was asked.",
            "example": 23
          },
          "counts": {
            "type": "object",
            "description": "The owner's post-write Q&A tallies, in the same shape and the same keys the Q&A list returns, so a client updates its pill badges without re-fetching.",
            "properties": {
              "total": {
                "type": "integer",
                "example": 26
              },
              "answered": {
                "type": "integer",
                "example": 10
              },
              "unanswered": {
                "type": "integer",
                "example": 16
              }
            }
          },
          "permissions": {
            "type": "object",
            "description": "Same block, and the same rules, as the Q&A list's `permissions`.",
            "properties": {
              "can_ask": {
                "type": "boolean",
                "example": true
              },
              "can_answer": {
                "type": "boolean",
                "example": true
              },
              "can_mark_best": {
                "type": "boolean",
                "example": false
              }
            }
          }
        }
      },
      "TrainingQaVoteState": {
        "type": "object",
        "description": "A question's or answer's upvote state AFTER a toggle, read back from the database. The toggle is NOT idempotent, so these are the only trustworthy values — do not predict them client-side.",
        "required": [
          "type",
          "id",
          "votes_count",
          "my_vote"
        ],
        "properties": {
          "type": {
            "type": "string",
            "description": "Which kind of row this is. SINGULAR, while the route segment is plural (`questions`/`answers`) — this API names a kind in a body singularly and in a path plurally throughout.",
            "enum": [
              "question",
              "answer"
            ],
            "example": "answer"
          },
          "id": {
            "type": "integer",
            "description": "The question's or answer's own id.",
            "example": 23
          },
          "votes_count": {
            "type": "integer",
            "description": "The row's upvote total after the toggle.",
            "example": 13
          },
          "my_vote": {
            "type": "boolean",
            "description": "Whether the caller now holds an upvote on it.",
            "example": true
          },
          "question_id": {
            "type": "integer",
            "description": "Present for an ANSWER only — the thread the row belongs to, so a client can locate the card without holding the mapping itself.",
            "example": 30
          }
        }
      },
      "TrainingDetailCta": {
        "type": "object",
        "description": "The screen's primary action, following the web sidebar / registration-card ladder. `lesson_id` is the lesson a resume/start lands on, so the client needs no second call to open the player — it is always the row flagged `current` in the returned `lessons[]`, never a lesson outside that outline.",
        "required": [
          "action"
        ],
        "properties": {
          "action": {
            "type": "string",
            "enum": [
              "resume",
              "start",
              "review",
              "view_certificate",
              "enroll",
              "buy",
              "choose_session",
              "view_session",
              "start_path",
              "continue_step",
              "view_steps",
              "locked"
            ],
            "example": "resume"
          },
          "lesson_id": {
            "type": "integer",
            "nullable": true,
            "description": "resume/start only.",
            "example": 130
          },
          "quiz": {
            "type": "boolean",
            "description": "resume/start only — the target lesson is a quiz.",
            "example": false
          },
          "certificate_id": {
            "type": "integer",
            "nullable": true,
            "description": "view_certificate only.",
            "example": 64
          },
          "price": {
            "type": "string",
            "nullable": true,
            "description": "buy only.",
            "example": "$49.00"
          },
          "step_position": {
            "type": "integer",
            "nullable": true,
            "description": "continue_step only (1-based).",
            "example": 2
          },
          "reason": {
            "type": "string",
            "nullable": true,
            "description": "locked only. `admin_enrolls` — self-enrollment is not offered for this item, an administrator enrolls learners. `purchase_required` — a priced course on a tenant that does not sell courses natively (the price still rides in the payload's own `price` / `free`), so asking an admin is the wrong instruction. Both are values the course detail sends; listed in full because a closed enum in a generated client fails the whole response on an unknown one.",
            "enum": [
              "admin_enrolls",
              "purchase_required"
            ],
            "example": "admin_enrolls"
          }
        }
      },
      "UnreadNotificationCount": {
        "type": "integer",
        "description": "**Piggyback field, returned by EVERY endpoint** that renders through the\nAPI base controller's `render_with_piggyback` — which is effectively all\nof `/api/v1` (including every Ideas endpoint, `/apps`, `/home`, and the\nrest). It is documented once here and `$ref`-ed where relevant rather\nthan repeated per endpoint.\n\nThe value is the calling user's count of unread, active notifications in\nthe current business — the number native clients paint on the app badge,\nwhich is why it rides along on unrelated responses instead of forcing a\nseparate `/notifications/count` call.\n\nGotchas:\n\n* Present only when the request resolved BOTH a user and a business; an\n  unauthenticated/business-less response omits it entirely.\n* Degrades to `0` (never an error) if the count query fails.\n* It is a snapshot at response time — do not treat it as a delta, and do\n  not assume it reflects any notification created by the same request.\n",
        "example": 3
      },
      "IdeaLifecycleStage": {
        "type": "object",
        "description": "One Ideas lifecycle stage. Emitted identically wherever a stage appears —\nthe ordered pipeline in `GET /ideas/config` and the stage reported by\n`PATCH /ideas/{idea_id}/stage` — because both render the same shared\npayload, so one parser handles both.\n",
        "required": [
          "id",
          "name",
          "category",
          "color",
          "icon",
          "position"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "Stable across a rename — safe to cache and to match against an idea's `stage.id`.",
            "example": 412
          },
          "name": {
            "type": "string",
            "description": "The admin-editable stage label.",
            "example": "Reviewing"
          },
          "category": {
            "type": "string",
            "enum": [
              "entry",
              "active",
              "implemented",
              "declined"
            ],
            "description": "What outcome metrics key off — never the id. Exactly one stage is `entry` (where new ideas land); `implemented` / `declined` are terminal outcomes, and moving INTO a `declined` stage records the off-ramp.",
            "example": "active"
          },
          "color": {
            "type": "string",
            "description": "Hex color for the stage pill.",
            "example": "#997404"
          },
          "icon": {
            "type": "string",
            "description": "Font Awesome glyph name with **no `fa-` prefix** (the same convention as `campaign.icon` and `vote_icon`), so prefix it the way you prefix those.\nDERIVED, not stored — there is no icon column and admins never pick one. The stage's LABEL wins when it is one of the two the mockup specifies (`Reviewing` → `magnifying-glass`, `Planned` → `calendar`); otherwise the CATEGORY decides: `entry` → `inbox`, `implemented` → `circle-check`, `declined` → `circle-xmark`, and `active` (or anything unrecognised) → `circle-dot`. Renaming or re-categorising a stage therefore changes its icon, and it is always the same glyph the web renders for that stage.",
            "example": "magnifying-glass"
          },
          "position": {
            "type": "integer",
            "description": "0-based pipeline position.",
            "example": 1
          }
        }
      },
      "IdeasAudience": {
        "type": "object",
        "description": "An Ideas audience setting — who a capability is open to. Used by\n`submit_audience` (who can submit ideas) and `campaign_creators` (who can\ncreate campaigns) in `GET /ideas/config`.\n\n`type: \"all\"` means everyone in the business. `type: \"group\"` means only\nmembers of `group`. `group` is `null` when the saved group has since been\ndeleted or belongs to another business — the runtime gate DENIES in that\ncase, so always drive affordances off the paired `can_*` boolean rather\nthan inferring permission from `type`.\n",
        "required": [
          "value",
          "type",
          "group"
        ],
        "properties": {
          "value": {
            "type": "string",
            "description": "The raw saved setting — `\"all\"`, or the group id as a string.",
            "example": "all"
          },
          "type": {
            "type": "string",
            "enum": [
              "all",
              "group"
            ],
            "example": "all"
          },
          "group": {
            "type": "object",
            "nullable": true,
            "description": "The audience group; null for `type: \"all\"`, or when the saved group no longer resolves.",
            "required": [
              "id",
              "name"
            ],
            "properties": {
              "id": {
                "type": "integer",
                "example": 44387
              },
              "name": {
                "type": "string",
                "example": "Idea Submitters"
              }
            }
          }
        }
      },
      "IdeasReviewPanel": {
        "type": "object",
        "description": "An Ideas review panel — the group whose members score ideas and move them\nthrough the lifecycle. Used by `idea_reviewers` and `campaign_reviewers` in\n`GET /ideas/config`.\n\n`count` is the panel's FULL membership size. This node **names nobody** —\nthere is no `members` key, and the shape is identical whatever\n`reviewer_names_visible` says. Render \"Reviewed by <group.name> (<count>)\"\nfrom it, and call a roster endpoint — both searchable, paginated, and gated on\n`reviewer_names_visible` — when you need the actual people:\n`GET /ideas/{idea_id}/reviewers` for a specific idea's panel, and\n`GET /ideas/campaigns/{id}/reviewers` for a specific campaign's.\n",
        "required": [
          "group",
          "count",
          "source"
        ],
        "properties": {
          "group": {
            "type": "object",
            "nullable": true,
            "description": "The panel group; null only when nothing resolves (no saved group and no All Admins fallback), in which case `count` is 0.",
            "required": [
              "id",
              "name"
            ],
            "properties": {
              "id": {
                "type": "integer",
                "example": 44390
              },
              "name": {
                "type": "string",
                "example": "Idea Reviewers"
              }
            }
          },
          "count": {
            "type": "integer",
            "description": "The panel's full member count (not the size of `members`).",
            "example": 8
          },
          "source": {
            "type": "string",
            "enum": [
              "configured",
              "fallback",
              "inherited"
            ],
            "description": "Where the panel came from:\n* `configured` — an admin picked this group in Settings.\n* `fallback` — no group is saved, so ideas route to the built-in\n  **All Admins** group (`idea_reviewers` only).\n* `inherited` — no campaign panel is saved, so a new campaign mirrors\n  `idea_reviewers` (`campaign_reviewers` only).\n",
            "example": "configured"
          }
        }
      },
      "FormSummary": {
        "type": "object",
        "description": "Summary information for a form template",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique form template ID",
            "example": 123
          },
          "name": {
            "type": "string",
            "description": "Form name",
            "example": "Incident Report"
          },
          "description": {
            "type": "string",
            "description": "Form description",
            "example": "Report workplace incidents and safety concerns"
          },
          "category": {
            "type": "string",
            "description": "Form category",
            "example": "safety"
          },
          "priority": {
            "type": "string",
            "enum": [
              "urgent",
              "high",
              "normal",
              "low"
            ],
            "description": "Form priority level",
            "example": "high"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "published",
              "archived"
            ],
            "description": "Template status",
            "example": "published"
          },
          "field_count": {
            "type": "integer",
            "description": "Total number of fields in the form",
            "example": 8
          },
          "required_field_count": {
            "type": "integer",
            "description": "Number of required fields",
            "example": 5
          },
          "estimated_time_minutes": {
            "type": "integer",
            "description": "Estimated completion time in minutes",
            "example": 10
          },
          "has_file_uploads": {
            "type": "boolean",
            "description": "Whether the form supports file uploads",
            "example": true
          },
          "sharing_enabled": {
            "type": "boolean",
            "description": "Whether the form can be shared publicly",
            "example": false
          },
          "requires_approval": {
            "type": "boolean",
            "description": "Whether submissions require approval",
            "example": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Form creation timestamp",
            "example": "2024-01-15T10:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "Last update timestamp",
            "example": "2024-01-15T10:00:00Z"
          }
        },
        "required": [
          "id",
          "name",
          "category",
          "priority",
          "status",
          "field_count",
          "estimated_time_minutes"
        ]
      },
      "FormTemplate": {
        "type": "object",
        "description": "Form data optimized for rendering",
        "properties": {
          "template": {
            "$ref": "#/components/schemas/FormTemplate"
          },
          "draft_submission": {
            "$ref": "#/components/schemas/FormSubmission",
            "nullable": true,
            "description": "Existing draft submission for progress restoration. NOTE: the `submission_data` property is intentionally omitted here — each field in `fields[]` now carries its own `submission_data` (sourced from this draft or the requested submission), so the map is not repeated on this object."
          },
          "mobile_config": {
            "type": "object",
            "description": "Mobile-specific rendering configuration",
            "properties": {
              "offline_capable": {
                "type": "boolean",
                "description": "Whether the form supports offline mode",
                "example": true
              },
              "voice_input_enabled": {
                "type": "boolean",
                "description": "Whether voice input is available",
                "example": true
              },
              "photo_capture_enabled": {
                "type": "boolean",
                "description": "Whether photo capture is available",
                "example": true
              },
              "gps_enabled": {
                "type": "boolean",
                "description": "Whether GPS location is required",
                "example": false
              },
              "estimated_time_minutes": {
                "type": "integer",
                "description": "Estimated completion time",
                "example": 10
              }
            }
          },
          "prefill_data": {
            "type": "object",
            "description": "Pre-filled field values",
            "nullable": true,
            "additionalProperties": true
          }
        },
        "required": [
          "template",
          "mobile_config"
        ]
      },
      "FormField": {
        "type": "object",
        "description": "Individual form field definition",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique field ID",
            "example": 456
          },
          "field_name": {
            "type": "string",
            "description": "Internal field name (used for data storage)",
            "example": "incident_type"
          },
          "field_type": {
            "type": "string",
            "enum": [
              "text",
              "textarea",
              "number",
              "email",
              "phone",
              "url",
              "select",
              "multiselect",
              "checkbox",
              "radio",
              "date",
              "datetime",
              "time",
              "file",
              "image",
              "video",
              "audio",
              "signature",
              "location",
              "gps",
              "barcode",
              "qr_code"
            ],
            "description": "Field input type",
            "example": "select"
          },
          "label": {
            "type": "string",
            "description": "Field label for display",
            "example": "Incident Type"
          },
          "is_editable": {
            "type": "boolean",
            "description": "Whether the client should render this field as user-editable. False for externally-sourced/computed inputs (lookup, rest_api), the table layout block, and navigation-control fields (conditional_navigator, flow_controller, smart_button, progress_gate, page_jump, section_toggle); true for all other field types.",
            "example": true
          },
          "description": {
            "type": "string",
            "description": "Help text for the field",
            "nullable": true,
            "example": "Select the type of incident that occurred"
          },
          "placeholder": {
            "type": "string",
            "description": "Placeholder text",
            "nullable": true,
            "example": "Choose incident type..."
          },
          "required": {
            "type": "boolean",
            "description": "Whether the field is required",
            "example": true
          },
          "position": {
            "type": "integer",
            "description": "Field order position",
            "example": 1
          },
          "section": {
            "type": "string",
            "description": "Form section grouping",
            "nullable": true,
            "example": "incident_details"
          },
          "options": {
            "type": "array",
            "description": "Options for select/radio/checkbox fields using rich object format",
            "nullable": true,
            "items": {
              "type": "object",
              "properties": {
                "value": {
                  "type": "string",
                  "description": "Option value stored in database",
                  "example": "injury"
                },
                "label": {
                  "type": "string",
                  "description": "Option display label shown to users",
                  "example": "Injury"
                },
                "description": {
                  "type": "string",
                  "description": "Optional description for the option",
                  "nullable": true
                }
              },
              "required": [
                "value",
                "label"
              ]
            }
          },
          "validation_rules": {
            "type": "object",
            "description": "Field validation rules",
            "nullable": true,
            "properties": {
              "required": {
                "type": "boolean",
                "example": true
              },
              "min_length": {
                "type": "integer",
                "example": 10
              },
              "max_length": {
                "type": "integer",
                "example": 500
              },
              "min_value": {
                "type": "number",
                "example": 0
              },
              "max_value": {
                "type": "number",
                "example": 100
              },
              "pattern": {
                "type": "string",
                "example": "^[A-Za-z0-9]+$"
              },
              "unique": {
                "type": "boolean",
                "description": "\"Unique values only\" — when true, the same value may not be submitted twice for this field. Present only when enabled.",
                "example": true
              },
              "custom_message": {
                "type": "string",
                "description": "Custom validation message to show instead of the generic one when this field fails validation. Present only when configured.",
                "example": "Please enter a valid employee ID."
              }
            }
          },
          "conditional_logic": {
            "type": "object",
            "description": "Field conditional display logic",
            "nullable": true,
            "properties": {
              "show_if": {
                "type": "object",
                "properties": {
                  "field": {
                    "type": "string",
                    "description": "Field name to check"
                  },
                  "operator": {
                    "type": "string",
                    "enum": [
                      "equals",
                      "not_equals",
                      "contains",
                      "greater_than",
                      "less_than"
                    ]
                  },
                  "value": {
                    "type": "string",
                    "description": "Value to compare against"
                  }
                }
              }
            }
          },
          "configuration": {
            "type": "object",
            "description": "Field-specific configuration settings",
            "nullable": true,
            "additionalProperties": true
          },
          "submission_data": {
            "nullable": true,
            "description": "This field's saved value, ALWAYS present on GET /api/v1/forms/:id. Source: the submission named by `submission_id` (prefill for resume/edit) when supplied, otherwise the caller's DRAFT submission for this form. `null` when there is no such submission/draft data, or when the referenced submission/draft never answered this field. Type matches the field (string, number, array for galleries, object for file references). For media fields (file/image/video/audio/gallery/signature) each file reference is enriched with a resolved absolute `url` (alongside its `file_id`) so the client can render/download the previously uploaded file — a single object for file/image/video/audio/signature, an array of such objects for a gallery."
          }
        },
        "required": [
          "id",
          "field_name",
          "field_type",
          "label",
          "required",
          "position",
          "submission_data"
        ]
      },
      "CompensationProfile": {
        "type": "object",
        "description": "Employee compensation profile with current details and policies",
        "properties": {
          "id": {
            "type": "integer",
            "description": "User business ID",
            "example": 123
          },
          "employee": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "example": 456
              },
              "name": {
                "type": "string",
                "example": "John Doe"
              },
              "title": {
                "type": "string",
                "nullable": true,
                "example": "Software Engineer"
              },
              "hire_date": {
                "type": "string",
                "format": "date",
                "nullable": true,
                "example": "2023-01-15"
              }
            }
          },
          "current_compensation": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "salary",
                  "hourly",
                  "commission",
                  "contract"
                ],
                "example": "salary"
              },
              "annual_salary": {
                "type": "number",
                "nullable": true,
                "example": 75000.0
              },
              "hourly_rate": {
                "type": "number",
                "nullable": true,
                "example": 36.06
              },
              "currency": {
                "type": "string",
                "example": "USD"
              },
              "pay_frequency": {
                "type": "string",
                "enum": [
                  "weekly",
                  "biweekly",
                  "monthly",
                  "quarterly",
                  "annually"
                ],
                "example": "monthly"
              },
              "effective_date": {
                "type": "string",
                "format": "date",
                "nullable": true,
                "example": "2024-01-01"
              }
            }
          },
          "policies": {
            "type": "object",
            "properties": {
              "can_view_details": {
                "type": "boolean",
                "example": true
              },
              "can_request_changes": {
                "type": "boolean",
                "example": true
              },
              "request_frequency": {
                "type": "string",
                "enum": [
                  "annual",
                  "semi_annual",
                  "unlimited"
                ],
                "example": "annual"
              },
              "mobile_access_enabled": {
                "type": "boolean",
                "example": true
              }
            }
          },
          "next_review_eligible": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "example": "2024-12-01"
          },
          "last_updated": {
            "type": "object",
            "properties": {
              "date": {
                "type": "string",
                "format": "date-time",
                "example": "2024-01-15T10:30:00Z"
              },
              "by": {
                "type": "string",
                "example": "System"
              }
            }
          }
        }
      },
      "CompensationHistoryRecord": {
        "type": "object",
        "description": "Individual compensation change record",
        "properties": {
          "id": {
            "type": "integer",
            "example": 789
          },
          "change_date": {
            "type": "string",
            "format": "date-time",
            "example": "2024-01-15T10:30:00Z"
          },
          "effective_date": {
            "type": "string",
            "format": "date",
            "example": "2024-02-01"
          },
          "change_reason": {
            "type": "string",
            "example": "Annual merit increase"
          },
          "changed_by": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "example": 101
              },
              "name": {
                "type": "string",
                "example": "Jane Manager"
              }
            }
          },
          "changes": {
            "type": "object",
            "properties": {
              "compensation_type": {
                "type": "object",
                "properties": {
                  "from": {
                    "type": "string",
                    "nullable": true,
                    "example": "salary"
                  },
                  "to": {
                    "type": "string",
                    "nullable": true,
                    "example": "salary"
                  }
                }
              },
              "annual_salary": {
                "type": "object",
                "properties": {
                  "from": {
                    "type": "number",
                    "nullable": true,
                    "example": 70000.0
                  },
                  "to": {
                    "type": "number",
                    "nullable": true,
                    "example": 75000.0
                  }
                }
              },
              "hourly_rate": {
                "type": "object",
                "properties": {
                  "from": {
                    "type": "number",
                    "nullable": true,
                    "example": null
                  },
                  "to": {
                    "type": "number",
                    "nullable": true,
                    "example": null
                  }
                }
              },
              "pay_frequency": {
                "type": "object",
                "properties": {
                  "from": {
                    "type": "string",
                    "nullable": true,
                    "example": "monthly"
                  },
                  "to": {
                    "type": "string",
                    "nullable": true,
                    "example": "monthly"
                  }
                }
              },
              "currency": {
                "type": "object",
                "properties": {
                  "from": {
                    "type": "string",
                    "nullable": true,
                    "example": "USD"
                  },
                  "to": {
                    "type": "string",
                    "nullable": true,
                    "example": "USD"
                  }
                }
              }
            }
          },
          "change_summary": {
            "type": "string",
            "example": "Salary: $70,000 → $75,000"
          },
          "change_direction": {
            "type": "string",
            "enum": [
              "increase",
              "decrease",
              "neutral"
            ],
            "example": "increase"
          },
          "is_retroactive": {
            "type": "boolean",
            "example": false
          },
          "effective_soon": {
            "type": "boolean",
            "example": true
          }
        }
      },
      "CompensationInsights": {
        "type": "object",
        "description": "Personalized compensation insights and analytics",
        "properties": {
          "summary": {
            "type": "object",
            "properties": {
              "current_annualized_value": {
                "type": "number",
                "nullable": true,
                "example": 75000.0
              },
              "compensation_type": {
                "type": "string",
                "example": "salary"
              },
              "currency": {
                "type": "string",
                "example": "USD"
              },
              "tenure_months": {
                "type": "integer",
                "example": 18
              }
            }
          },
          "recent_activity": {
            "type": "object",
            "properties": {
              "changes_last_12_months": {
                "type": "integer",
                "example": 1
              },
              "last_change_date": {
                "type": "string",
                "format": "date",
                "nullable": true,
                "example": "2024-01-15"
              }
            }
          },
          "request_eligibility": {
            "type": "object",
            "properties": {
              "can_request_now": {
                "type": "boolean",
                "example": true
              },
              "next_eligible_date": {
                "type": "string",
                "format": "date",
                "nullable": true,
                "example": "2025-01-15"
              }
            }
          },
          "performance_link": {
            "type": "object",
            "properties": {
              "has_recent_review": {
                "type": "boolean",
                "example": false
              },
              "merit_increase_eligible": {
                "type": "boolean",
                "example": false
              }
            }
          }
        }
      },
      "CompensationCalculation": {
        "type": "object",
        "description": "Compensation calculation result",
        "properties": {
          "compensation_type": {
            "type": "string",
            "enum": [
              "salary",
              "hourly"
            ],
            "example": "salary"
          },
          "annual_salary": {
            "type": "number",
            "nullable": true,
            "example": 75000.0
          },
          "calculated_hourly_rate": {
            "type": "number",
            "nullable": true,
            "example": 36.06
          },
          "hourly_rate": {
            "type": "number",
            "nullable": true,
            "example": null
          },
          "calculated_annual_salary": {
            "type": "number",
            "nullable": true,
            "example": null
          },
          "annual_hours_basis": {
            "type": "number",
            "example": 2080.0
          },
          "currency": {
            "type": "string",
            "example": "USD"
          }
        }
      },
      "CompensationRequestSummary": {
        "type": "object",
        "description": "Summary view of compensation request",
        "properties": {
          "id": {
            "type": "integer",
            "example": 456
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "approved",
              "rejected",
              "cancelled"
            ],
            "example": "pending"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2024-01-20T14:30:00Z"
          },
          "effective_date": {
            "type": "string",
            "format": "date",
            "example": "2024-03-01"
          },
          "change_summary": {
            "type": "string",
            "example": "Salary: $75,000 → $80,000"
          },
          "change_impact": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "increase",
                  "decrease",
                  "neutral"
                ],
                "example": "increase"
              },
              "amount": {
                "type": "number",
                "example": 5000.0
              },
              "percentage": {
                "type": "number",
                "nullable": true,
                "example": 6.7
              },
              "description": {
                "type": "string",
                "example": "Increase of $5,000 (6.7%)"
              }
            }
          },
          "approver": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer",
                "example": 789
              },
              "name": {
                "type": "string",
                "example": "Jane Manager"
              }
            }
          },
          "days_pending": {
            "type": "integer",
            "example": 5
          },
          "is_urgent": {
            "type": "boolean",
            "example": false
          },
          "effective_soon": {
            "type": "boolean",
            "example": true
          }
        }
      },
      "CompensationRequestDetail": {
        "type": "object",
        "description": "Detailed view of compensation request",
        "properties": {
          "id": {
            "type": "integer",
            "example": 456
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "approved",
              "rejected",
              "cancelled"
            ],
            "example": "pending"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2024-01-20T14:30:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2024-01-20T14:30:00Z"
          },
          "effective_date": {
            "type": "string",
            "format": "date",
            "example": "2024-03-01"
          },
          "justification": {
            "type": "string",
            "example": "Requesting salary increase based on performance review and market analysis"
          },
          "current_compensation": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "example": "salary"
              },
              "annual_salary": {
                "type": "number",
                "nullable": true,
                "example": 75000.0
              },
              "hourly_rate": {
                "type": "number",
                "nullable": true,
                "example": null
              },
              "pay_frequency": {
                "type": "string",
                "example": "monthly"
              },
              "currency": {
                "type": "string",
                "example": "USD"
              }
            }
          },
          "requested_compensation": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "example": "salary"
              },
              "annual_salary": {
                "type": "number",
                "nullable": true,
                "example": 80000.0
              },
              "hourly_rate": {
                "type": "number",
                "nullable": true,
                "example": null
              },
              "pay_frequency": {
                "type": "string",
                "example": "monthly"
              },
              "currency": {
                "type": "string",
                "example": "USD"
              }
            }
          },
          "changes": {
            "type": "object",
            "properties": {
              "has_salary_change": {
                "type": "boolean",
                "example": true
              },
              "has_hourly_change": {
                "type": "boolean",
                "example": false
              },
              "has_type_change": {
                "type": "boolean",
                "example": false
              },
              "has_frequency_change": {
                "type": "boolean",
                "example": false
              },
              "has_currency_change": {
                "type": "boolean",
                "example": false
              },
              "summary": {
                "type": "string",
                "example": "Salary: $75,000 → $80,000"
              },
              "impact": {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "example": "increase"
                  },
                  "amount": {
                    "type": "number",
                    "example": 5000.0
                  },
                  "percentage": {
                    "type": "number",
                    "example": 6.7
                  },
                  "description": {
                    "type": "string",
                    "example": "Increase of $5,000 (6.7%)"
                  }
                }
              }
            }
          },
          "approval": {
            "type": "object",
            "properties": {
              "approver": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "integer",
                    "example": 789
                  },
                  "name": {
                    "type": "string",
                    "example": "Jane Manager"
                  }
                }
              },
              "approved_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "example": null
              },
              "rejected_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "example": null
              },
              "manager_notes": {
                "type": "string",
                "nullable": true,
                "example": null
              }
            }
          },
          "status_info": {
            "type": "object",
            "properties": {
              "days_pending": {
                "type": "integer",
                "example": 5
              },
              "is_urgent": {
                "type": "boolean",
                "example": false
              },
              "effective_soon": {
                "type": "boolean",
                "example": true
              },
              "can_edit": {
                "type": "boolean",
                "example": true
              },
              "can_cancel": {
                "type": "boolean",
                "example": true
              }
            }
          }
        }
      },
      "CompensationRequestInput": {
        "type": "object",
        "description": "Input schema for creating/updating compensation requests",
        "required": [
          "justification",
          "requested_compensation_type"
        ],
        "properties": {
          "justification": {
            "type": "string",
            "minLength": 10,
            "maxLength": 1000,
            "example": "Requesting salary increase based on performance review and market analysis"
          },
          "effective_date": {
            "type": "string",
            "format": "date",
            "example": "2024-03-01"
          },
          "requested_compensation_type": {
            "type": "string",
            "enum": [
              "salary",
              "hourly",
              "commission",
              "contract"
            ],
            "example": "salary"
          },
          "requested_annual_salary": {
            "type": "number",
            "minimum": 0,
            "nullable": true,
            "example": 80000.0
          },
          "requested_hourly_rate": {
            "type": "number",
            "minimum": 0,
            "nullable": true,
            "example": null
          },
          "requested_pay_frequency": {
            "type": "string",
            "enum": [
              "weekly",
              "biweekly",
              "monthly",
              "quarterly",
              "annually"
            ],
            "example": "monthly"
          },
          "requested_currency": {
            "type": "string",
            "example": "USD"
          }
        }
      },
      "CompensationRequestStatistics": {
        "type": "object",
        "description": "Statistics about employee's compensation requests",
        "properties": {
          "total": {
            "type": "integer",
            "example": 3
          },
          "pending": {
            "type": "integer",
            "example": 1
          },
          "approved": {
            "type": "integer",
            "example": 1
          },
          "rejected": {
            "type": "integer",
            "example": 0
          },
          "cancelled": {
            "type": "integer",
            "example": 1
          }
        }
      },
      "EPMSDashboard": {
        "type": "object",
        "description": "EPMS dashboard data with aggregated goals, reviews, feedback, and meetings",
        "properties": {
          "goals": {
            "type": "object",
            "properties": {
              "active_count": {
                "type": "integer",
                "example": 5
              },
              "completed_count": {
                "type": "integer",
                "example": 12
              },
              "overdue_count": {
                "type": "integer",
                "example": 2
              },
              "recent_updates": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/EPMSGoal"
                }
              }
            }
          },
          "reviews": {
            "type": "object",
            "properties": {
              "pending_count": {
                "type": "integer",
                "example": 1
              },
              "in_progress_count": {
                "type": "integer",
                "example": 2
              },
              "completed_count": {
                "type": "integer",
                "example": 8
              },
              "recent_reviews": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/EPMSPerformanceReview"
                }
              }
            }
          },
          "feedback": {
            "type": "object",
            "properties": {
              "received_count": {
                "type": "integer",
                "example": 15
              },
              "pending_acknowledgment": {
                "type": "integer",
                "example": 3
              },
              "recent_feedback": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/EPMSContinuousFeedback"
                }
              }
            }
          },
          "meetings": {
            "type": "object",
            "properties": {
              "upcoming_count": {
                "type": "integer",
                "example": 4
              },
              "recent_count": {
                "type": "integer",
                "example": 6
              },
              "recent_meetings": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/EPMSMeeting"
                }
              }
            }
          },
          "action_items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "type": {
                  "type": "string",
                  "enum": [
                    "goal_update",
                    "review_submit",
                    "feedback_acknowledge",
                    "meeting_schedule"
                  ]
                },
                "title": {
                  "type": "string"
                },
                "due_date": {
                  "type": "string",
                  "format": "date",
                  "nullable": true
                },
                "priority": {
                  "type": "string",
                  "enum": [
                    "low",
                    "medium",
                    "high",
                    "critical"
                  ]
                }
              }
            }
          }
        }
      },
      "EPMSTeamDashboard": {
        "type": "object",
        "description": "Team dashboard data for managers",
        "properties": {
          "team_stats": {
            "type": "object",
            "properties": {
              "team_size": {
                "type": "integer",
                "example": 12
              },
              "active_goals": {
                "type": "integer",
                "example": 45
              },
              "pending_reviews": {
                "type": "integer",
                "example": 8
              },
              "pending_approvals": {
                "type": "integer",
                "example": 5
              }
            }
          },
          "team_goals": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EPMSGoal"
            }
          },
          "pending_reviews": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EPMSPerformanceReview"
            }
          },
          "team_feedback": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EPMSContinuousFeedback"
            }
          },
          "upcoming_meetings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EPMSMeeting"
            }
          }
        }
      },
      "EPMSGoal": {
        "type": "object",
        "description": "Employee goal with progress tracking. Progress updates are returned as a sibling array in GET /goals/{id}, not nested in the goal object.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 123
          },
          "title": {
            "type": "string",
            "example": "Increase sales by 20%"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "example": "Achieve 20% growth in Q1 sales"
          },
          "goal_type": {
            "type": "string",
            "enum": [
              "performance",
              "development",
              "behavior",
              "project",
              "skill"
            ],
            "example": "performance"
          },
          "goal_type_label": {
            "type": "string",
            "nullable": true,
            "description": "Human-readable label for goal type"
          },
          "goal_category": {
            "type": "string",
            "nullable": true,
            "description": "The focus area category key for this goal",
            "example": "professional_development"
          },
          "goal_category_label": {
            "type": "string",
            "nullable": true,
            "description": "Human-readable label for the goal category",
            "example": "Professional Development"
          },
          "priority": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high",
              "critical"
            ],
            "example": "high"
          },
          "priority_label": {
            "type": "string",
            "nullable": true,
            "description": "Human-readable label for priority"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "in_review",
              "active",
              "on_hold",
              "completed",
              "cancelled",
              "overdue"
            ],
            "example": "active"
          },
          "display_status": {
            "type": "string",
            "nullable": true,
            "description": "Human-readable display status"
          },
          "start_date": {
            "type": "string",
            "format": "date",
            "example": "2026-01-01"
          },
          "target_date": {
            "type": "string",
            "format": "date",
            "example": "2026-03-31"
          },
          "completed_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "progress_percentage": {
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "example": 45.5
          },
          "weight_percentage": {
            "type": "number",
            "nullable": true,
            "example": 30.0
          },
          "workflow_stage": {
            "type": "string",
            "enum": [
              "draft",
              "employee_review",
              "manager_finalized",
              "leadership_approved"
            ],
            "description": "Current workflow stage"
          },
          "workflow_stage_label": {
            "type": "string",
            "nullable": true
          },
          "is_smart_goal": {
            "type": "boolean",
            "example": true
          },
          "smart_score": {
            "type": "number",
            "nullable": true
          },
          "on_track": {
            "type": "boolean",
            "nullable": true
          },
          "days_until_due": {
            "type": "integer",
            "nullable": true
          },
          "is_overdue": {
            "type": "boolean",
            "example": false
          },
          "progress_update_allowed": {
            "type": "boolean",
            "description": "Whether progress updates can be submitted for this goal"
          },
          "success_criteria": {
            "type": "string",
            "nullable": true,
            "description": "Present when include_details is true (e.g. show endpoint)",
            "example": "Reach $500K in sales"
          },
          "smart_criteria": {
            "type": "object",
            "nullable": true,
            "description": "Present when include_details is true",
            "properties": {
              "is_specific": {
                "type": "boolean"
              },
              "is_measurable": {
                "type": "boolean"
              },
              "is_achievable": {
                "type": "boolean"
              },
              "is_relevant": {
                "type": "boolean"
              },
              "is_time_bound": {
                "type": "boolean"
              }
            }
          },
          "progress_updates_count": {
            "type": "integer",
            "description": "Present when include_details is true"
          },
          "latest_progress_update": {
            "$ref": "#/components/schemas/EPMSProgressUpdate",
            "nullable": true,
            "description": "Present when include_details is true"
          },
          "can_edit": {
            "type": "boolean",
            "description": "Present when include_details is true"
          },
          "can_complete": {
            "type": "boolean",
            "description": "Present when include_details is true"
          },
          "can_delete": {
            "type": "boolean",
            "description": "Present when include_details is true"
          },
          "requires_manager_approval": {
            "type": "boolean",
            "description": "Present when include_details is true"
          },
          "is_fully_approved": {
            "type": "boolean",
            "description": "Present when include_details is true"
          },
          "in_review_stage": {
            "type": "boolean",
            "description": "Present when include_details is true"
          },
          "can_cancel": {
            "type": "boolean",
            "description": "Present when include_details is true"
          },
          "can_put_on_hold": {
            "type": "boolean",
            "description": "Present when include_details is true"
          },
          "can_reactivate": {
            "type": "boolean",
            "description": "Present when include_details is true"
          },
          "can_send_to_employee": {
            "type": "boolean",
            "description": "Present when include_details is true"
          },
          "can_employee_review": {
            "type": "boolean",
            "description": "Present when include_details is true"
          },
          "can_manager_finalize": {
            "type": "boolean",
            "description": "Present when include_details is true"
          },
          "can_leadership_approve": {
            "type": "boolean",
            "description": "Present when include_details is true"
          },
          "manager_approved_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Present when include_details is true"
          },
          "leadership_approved_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Present when include_details is true"
          },
          "employee": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer",
                "example": 456
              },
              "name": {
                "type": "string",
                "example": "John Doe"
              },
              "email": {
                "type": "string",
                "nullable": true,
                "example": "john@example.com"
              },
              "job_title": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-01-01T10:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-01-15T14:30:00Z"
          }
        }
      },
      "EPMSGoalInput": {
        "type": "object",
        "description": "Input schema for creating/updating goals",
        "required": [
          "title"
        ],
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255,
            "example": "Increase sales by 20%"
          },
          "description": {
            "type": "string",
            "maxLength": 2000,
            "example": "Achieve 20% growth in Q1 sales"
          },
          "goal_type": {
            "type": "string",
            "enum": [
              "performance",
              "development",
              "behavior",
              "project",
              "skill"
            ],
            "example": "performance"
          },
          "goal_category": {
            "type": "string",
            "maxLength": 50,
            "description": "Focus area category key. Required on create for non-department goals. Must be a valid category configured for the business. Use `GET /epms/goals/categories` to retrieve valid values. Default categories for IC: professional_development, lead_self, work_with_others, contribute_to_business. Default categories for leaders: professional_development, lead_self, lead_others, lead_the_business. Business configuration may differ.\n",
            "example": "professional_development"
          },
          "priority": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high",
              "critical"
            ],
            "example": "high"
          },
          "start_date": {
            "type": "string",
            "format": "date",
            "example": "2026-01-01"
          },
          "target_date": {
            "type": "string",
            "format": "date",
            "example": "2026-03-31"
          },
          "progress_percentage": {
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "example": 0
          },
          "weight_percentage": {
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "example": 30.0
          },
          "success_criteria": {
            "type": "string",
            "maxLength": 500,
            "example": "Reach $500K in sales"
          },
          "employee_id": {
            "type": "integer",
            "example": 456
          },
          "is_specific": {
            "type": "boolean",
            "example": true
          },
          "is_measurable": {
            "type": "boolean",
            "example": true
          },
          "is_achievable": {
            "type": "boolean",
            "example": true
          },
          "is_relevant": {
            "type": "boolean",
            "example": true
          },
          "is_time_bound": {
            "type": "boolean",
            "example": true
          }
        }
      },
      "EPMSGoalCategoryOption": {
        "type": "object",
        "description": "A goal category option with value and human-readable label",
        "properties": {
          "value": {
            "type": "string",
            "description": "Category key to send on create/update",
            "example": "professional_development"
          },
          "label": {
            "type": "string",
            "description": "Human-readable label for display",
            "example": "Professional Development"
          }
        }
      },
      "EPMSGoalCategoriesResponse": {
        "type": "object",
        "description": "Response containing valid goal categories for a user's role",
        "properties": {
          "categories": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EPMSGoalCategoryOption"
            }
          }
        }
      },
      "EPMSGoalTemplateOption": {
        "type": "object",
        "description": "A goal template available for prefilling the create form",
        "properties": {
          "id": {
            "type": "integer",
            "example": 1
          },
          "title": {
            "type": "string",
            "example": "Improve Task Completion"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "example": "Focus on completing tasks on time and improving quality."
          },
          "goal_type": {
            "type": "string",
            "example": "performance"
          },
          "priority": {
            "type": "string",
            "example": "medium"
          },
          "success_criteria": {
            "type": "string",
            "nullable": true,
            "example": "Task completion rate above 90%"
          },
          "resources_needed": {
            "type": "string",
            "nullable": true,
            "example": "Project management tool access"
          },
          "measurement_method": {
            "type": "string",
            "nullable": true,
            "example": "Weekly task completion reports"
          },
          "is_default": {
            "type": "boolean",
            "example": true
          },
          "smart_criteria": {
            "$ref": "#/components/schemas/EPMSSmartCriteria"
          }
        }
      },
      "EPMSGoalChecklistItem": {
        "type": "object",
        "description": "A SMART checklist item",
        "properties": {
          "key": {
            "type": "string",
            "example": "is_specific"
          },
          "label": {
            "type": "string",
            "example": "Specific"
          }
        }
      },
      "EPMSGoalChecklist": {
        "type": "object",
        "description": "SMART goal checklist configuration",
        "properties": {
          "enforced": {
            "type": "boolean",
            "description": "Whether the SMART checklist is enforced for goal creation",
            "example": false
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EPMSGoalChecklistItem"
            }
          }
        }
      },
      "EPMSGoalCreationConfigResponse": {
        "type": "object",
        "description": "All dynamic properties needed to render the goal create form",
        "properties": {
          "goal_templates": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EPMSGoalTemplateOption"
            }
          },
          "goal_types": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EPMSGoalCategoryOption"
            }
          },
          "priorities": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EPMSGoalCategoryOption"
            }
          },
          "goal_categories": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EPMSGoalCategoryOption"
            }
          },
          "goal_checklist": {
            "$ref": "#/components/schemas/EPMSGoalChecklist"
          },
          "requires_approval": {
            "type": "boolean",
            "example": true
          }
        }
      },
      "EPMSSmartCriterionResult": {
        "type": "object",
        "properties": {
          "score": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10,
            "example": 8
          },
          "feedback": {
            "type": "string",
            "example": "The goal is specific and clear"
          },
          "suggestion": {
            "type": "string",
            "example": "Include specific teams responsible"
          }
        }
      },
      "EPMSSmartAnalysisResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          },
          "overall_score": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100,
            "example": 75
          },
          "criteria": {
            "type": "object",
            "properties": {
              "specific": {
                "$ref": "#/components/schemas/EPMSSmartCriterionResult"
              },
              "measurable": {
                "$ref": "#/components/schemas/EPMSSmartCriterionResult"
              },
              "achievable": {
                "$ref": "#/components/schemas/EPMSSmartCriterionResult"
              },
              "relevant": {
                "$ref": "#/components/schemas/EPMSSmartCriterionResult"
              },
              "time_bound": {
                "$ref": "#/components/schemas/EPMSSmartCriterionResult"
              }
            }
          },
          "improved_title": {
            "type": "string",
            "nullable": true
          },
          "improved_description": {
            "type": "string",
            "nullable": true
          },
          "summary": {
            "type": "string",
            "nullable": true
          },
          "goal": {
            "$ref": "#/components/schemas/EPMSGoal"
          }
        }
      },
      "EPMSSmartTextAnalysisResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          },
          "overall_score": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "criteria": {
            "type": "object",
            "properties": {
              "specific": {
                "$ref": "#/components/schemas/EPMSSmartCriterionResult"
              },
              "measurable": {
                "$ref": "#/components/schemas/EPMSSmartCriterionResult"
              },
              "achievable": {
                "$ref": "#/components/schemas/EPMSSmartCriterionResult"
              },
              "relevant": {
                "$ref": "#/components/schemas/EPMSSmartCriterionResult"
              },
              "time_bound": {
                "$ref": "#/components/schemas/EPMSSmartCriterionResult"
              }
            }
          },
          "improved_title": {
            "type": "string",
            "nullable": true
          },
          "improved_description": {
            "type": "string",
            "nullable": true
          },
          "summary": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "EPMSGoalTemplate": {
        "type": "object",
        "description": "Goal template summary",
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "goal_type": {
            "type": "string"
          },
          "goal_type_label": {
            "type": "string"
          },
          "priority": {
            "type": "string"
          },
          "priority_label": {
            "type": "string"
          },
          "is_default": {
            "type": "boolean"
          },
          "template_type": {
            "type": "string",
            "enum": [
              "system",
              "custom"
            ]
          },
          "smart_score": {
            "type": "integer"
          },
          "is_smart_template": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "EPMSGoalTemplateDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/EPMSGoalTemplate"
          },
          {
            "type": "object",
            "properties": {
              "success_criteria": {
                "type": "string",
                "nullable": true
              },
              "resources_needed": {
                "type": "string",
                "nullable": true
              },
              "measurement_method": {
                "type": "string",
                "nullable": true
              },
              "smart_criteria": {
                "type": "object",
                "properties": {
                  "is_specific": {
                    "type": "boolean"
                  },
                  "is_measurable": {
                    "type": "boolean"
                  },
                  "is_achievable": {
                    "type": "boolean"
                  },
                  "is_relevant": {
                    "type": "boolean"
                  },
                  "is_time_bound": {
                    "type": "boolean"
                  }
                }
              },
              "missing_smart_criteria": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "usage_statistics": {
                "type": "object",
                "properties": {
                  "total_goals": {
                    "type": "integer"
                  },
                  "active_goals": {
                    "type": "integer"
                  },
                  "completed_goals": {
                    "type": "integer"
                  },
                  "completion_rate": {
                    "type": "number"
                  },
                  "last_used": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true
                  }
                }
              }
            }
          }
        ]
      },
      "EPMSGoalTemplateListResponse": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EPMSGoalTemplate"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "EPMSProgressUpdate": {
        "type": "object",
        "description": "Goal progress update record",
        "properties": {
          "id": {
            "type": "integer",
            "example": 789
          },
          "progress_percentage": {
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "example": 50.0
          },
          "update_notes": {
            "type": "string",
            "nullable": true,
            "example": "Halfway through the quarter, on track"
          },
          "update_date": {
            "type": "string",
            "format": "date",
            "example": "2026-01-15"
          },
          "update_type": {
            "type": "string",
            "enum": [
              "regular",
              "milestone",
              "completion",
              "revision",
              "comment"
            ],
            "example": "regular"
          },
          "challenges_faced": {
            "type": "string",
            "nullable": true
          },
          "support_needed": {
            "type": "string",
            "nullable": true
          },
          "attachments": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true
          },
          "updated_by": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "example": 456
              },
              "name": {
                "type": "string",
                "example": "John Doe"
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-01-15T10:30:00Z"
          }
        }
      },
      "EPMSProgressUpdateInput": {
        "type": "object",
        "description": "Input schema for progress updates",
        "properties": {
          "progress_percentage": {
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "description": "Required for all types except 'comment'",
            "example": 50.0
          },
          "update_notes": {
            "type": "string",
            "maxLength": 1000,
            "example": "Halfway through the quarter, on track"
          },
          "update_type": {
            "type": "string",
            "enum": [
              "regular",
              "milestone",
              "completion",
              "revision",
              "comment"
            ],
            "default": "regular",
            "example": "regular"
          },
          "challenges_faced": {
            "type": "string",
            "nullable": true,
            "description": "Obstacles or challenges encountered",
            "example": "Dependency on external vendor delayed delivery"
          },
          "support_needed": {
            "type": "string",
            "nullable": true,
            "description": "Support or resources needed",
            "example": "Need additional budget approval for tooling"
          },
          "attachments": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true,
            "description": "File references/attachment identifiers"
          }
        }
      },
      "EPMSGoalStats": {
        "type": "object",
        "description": "Goal statistics for dashboard",
        "properties": {
          "total_goals": {
            "type": "integer",
            "example": 20
          },
          "active_goals": {
            "type": "integer",
            "example": 12
          },
          "completed_goals": {
            "type": "integer",
            "example": 6
          },
          "overdue_goals": {
            "type": "integer",
            "example": 2
          },
          "on_hold_goals": {
            "type": "integer",
            "example": 0
          },
          "cancelled_goals": {
            "type": "integer",
            "example": 0
          },
          "due_soon_count": {
            "type": "integer",
            "description": "Goals due within the next 7 days",
            "example": 3
          },
          "average_progress": {
            "type": "number",
            "example": 45.5
          },
          "completion_rate": {
            "type": "number",
            "description": "Percentage of completed goals (0-100)",
            "example": 30.0
          },
          "by_status": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Counts per status (draft, active, on_hold, completed, cancelled, in_review, overdue)"
          },
          "by_priority": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Counts per priority (low, medium, high, critical)"
          },
          "by_type": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Counts per goal type (performance, development, behavior, project, skill)"
          }
        }
      },
      "EPMSPerformanceReview": {
        "type": "object",
        "description": "Performance review with assessment details",
        "properties": {
          "id": {
            "type": "integer",
            "example": 234
          },
          "title": {
            "type": "string",
            "example": "Q1 2026 Performance Review"
          },
          "review_type": {
            "type": "string",
            "enum": [
              "annual",
              "quarterly",
              "probation",
              "project",
              "performance_improvement"
            ],
            "example": "quarterly"
          },
          "status": {
            "type": "string",
            "enum": [
              "created",
              "in_progress",
              "submitted",
              "approved",
              "completed",
              "cancelled"
            ],
            "example": "in_progress"
          },
          "display_status": {
            "type": "string",
            "description": "Computed status that accounts for overdue reviews",
            "example": "in_progress"
          },
          "status_color": {
            "type": "string",
            "description": "Bootstrap badge color variant for the display status",
            "enum": [
              "info",
              "primary",
              "success",
              "danger",
              "secondary"
            ],
            "example": "primary"
          },
          "review_period_start": {
            "type": "string",
            "format": "date",
            "example": "2026-01-01"
          },
          "review_period_end": {
            "type": "string",
            "format": "date",
            "example": "2026-03-31"
          },
          "due_date": {
            "type": "string",
            "format": "date",
            "example": "2026-04-15"
          },
          "employee": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "example": 456
              },
              "name": {
                "type": "string",
                "example": "John Doe"
              }
            }
          },
          "manager": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer",
                "example": 789
              },
              "name": {
                "type": "string",
                "example": "Jane Manager"
              }
            }
          },
          "overall_rating": {
            "type": "number",
            "nullable": true,
            "minimum": 0,
            "maximum": 5,
            "example": 4.5
          },
          "manager_comments": {
            "type": "string",
            "nullable": true,
            "example": "Great progress this quarter"
          },
          "self_assessment": {
            "type": "object",
            "nullable": true,
            "properties": {
              "overall_summary": {
                "type": "string"
              },
              "strengths": {
                "type": "string"
              },
              "areas_for_improvement": {
                "type": "string"
              },
              "self_rating": {
                "type": "number",
                "minimum": 0,
                "maximum": 5
              }
            }
          },
          "linked_goals": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EPMSGoal"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-01-01T10:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-01-15T14:30:00Z"
          },
          "is_overdue": {
            "type": "boolean",
            "example": false
          }
        }
      },
      "EPMSPerformanceReviewInput": {
        "type": "object",
        "description": "Input schema for creating/updating performance reviews",
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255,
            "example": "Q1 2026 Performance Review"
          },
          "employee_id": {
            "type": "integer",
            "example": 456
          },
          "review_type": {
            "type": "string",
            "enum": [
              "annual",
              "quarterly",
              "probation",
              "project",
              "performance_improvement"
            ],
            "example": "quarterly"
          },
          "review_period_start": {
            "type": "string",
            "format": "date",
            "example": "2026-01-01"
          },
          "review_period_end": {
            "type": "string",
            "format": "date",
            "example": "2026-03-31"
          },
          "due_date": {
            "type": "string",
            "format": "date",
            "example": "2026-04-15"
          },
          "performance_review_template_id": {
            "type": "integer",
            "nullable": true
          },
          "manager_comments": {
            "type": "string",
            "maxLength": 5000,
            "example": "Great progress this quarter"
          },
          "overall_rating": {
            "type": "number",
            "minimum": 0,
            "maximum": 5,
            "example": 4.5
          }
        }
      },
      "EPMSSelfAssessmentInput": {
        "type": "object",
        "description": "Input schema for self-assessment submission",
        "properties": {
          "overall_summary": {
            "type": "string",
            "maxLength": 2000,
            "example": "I achieved all my goals this quarter"
          },
          "strengths": {
            "type": "string",
            "maxLength": 1000,
            "example": "Strong communication and leadership"
          },
          "areas_for_improvement": {
            "type": "string",
            "maxLength": 1000,
            "example": "Time management"
          },
          "accomplishments": {
            "type": "string",
            "maxLength": 2000,
            "example": "Completed 3 major projects"
          },
          "goals_reflection": {
            "type": "string",
            "maxLength": 1000,
            "example": "Met 90% of goals"
          },
          "training_needs": {
            "type": "string",
            "maxLength": 500,
            "example": "Advanced project management"
          },
          "career_aspirations": {
            "type": "string",
            "maxLength": 1000,
            "example": "Lead a larger team"
          },
          "self_rating": {
            "type": "number",
            "minimum": 0,
            "maximum": 5,
            "example": 4.0
          },
          "ratings": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "category": {
                  "type": "string",
                  "example": "Communication"
                },
                "rating": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 5,
                  "example": 4.5
                },
                "comments": {
                  "type": "string",
                  "example": "Excellent"
                }
              }
            }
          },
          "goals": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "goal_id": {
                  "type": "integer",
                  "example": 123
                },
                "achievement_notes": {
                  "type": "string",
                  "example": "Exceeded expectations"
                },
                "self_rating": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 5,
                  "example": 5.0
                }
              }
            }
          }
        }
      },
      "EPMSPerformanceReviewStats": {
        "type": "object",
        "description": "Performance review statistics",
        "properties": {
          "total_reviews": {
            "type": "integer",
            "example": 15
          },
          "pending_reviews": {
            "type": "integer",
            "example": 5
          },
          "completed_reviews": {
            "type": "integer",
            "example": 8
          },
          "overdue_reviews": {
            "type": "integer",
            "example": 2
          },
          "due_soon_count": {
            "type": "integer",
            "example": 3
          },
          "completion_rate": {
            "type": "number",
            "example": 53.3
          },
          "by_status": {
            "type": "object",
            "description": "Count of reviews by status key",
            "additionalProperties": {
              "type": "integer"
            },
            "example": {
              "created": 2,
              "in_progress": 5,
              "completed": 3,
              "approved": 2,
              "archived": 1
            }
          },
          "by_type": {
            "type": "object",
            "description": "Count of reviews by review_type key",
            "additionalProperties": {
              "type": "integer"
            },
            "example": {
              "annual": 8,
              "quarterly": 5,
              "ad_hoc": 2
            }
          },
          "average_rating": {
            "type": "number",
            "nullable": true,
            "example": 4.2
          }
        }
      },
      "EPMSMyReviewSummary": {
        "type": "object",
        "description": "Personal review summary for the current user (matches \"My Review Status\" widget)",
        "properties": {
          "total_reviews": {
            "type": "integer",
            "description": "Total number of reviews where the user is the employee",
            "example": 5
          },
          "completed_reviews": {
            "type": "integer",
            "description": "Reviews with status completed or approved",
            "example": 3
          },
          "pending_reviews": {
            "type": "integer",
            "description": "Reviews with status created or in_progress",
            "example": 1
          },
          "average_rating": {
            "type": "number",
            "description": "Average overall_rating across completed/approved reviews (rounded to 1 decimal, defaults to 0)",
            "example": 4.2
          },
          "has_active_review": {
            "type": "boolean",
            "description": "Whether the user has an active (created or in_progress) review",
            "example": true
          },
          "current_review": {
            "type": "object",
            "nullable": true,
            "description": "The most recent active review summary",
            "properties": {
              "id": {
                "type": "integer"
              },
              "title": {
                "type": "string"
              },
              "status": {
                "type": "string"
              },
              "display_status": {
                "type": "string"
              },
              "status_color": {
                "type": "string",
                "enum": [
                  "info",
                  "primary",
                  "success",
                  "danger",
                  "secondary"
                ]
              },
              "start_date": {
                "type": "string",
                "format": "date",
                "nullable": true
              },
              "due_date": {
                "type": "string",
                "format": "date",
                "nullable": true
              },
              "days_until_due": {
                "type": "integer",
                "nullable": true
              },
              "completion_percentage": {
                "type": "integer"
              },
              "requires_self_assessment": {
                "type": "boolean"
              },
              "self_assessment_completed": {
                "type": "boolean"
              }
            }
          },
          "pending_actions": {
            "type": "array",
            "description": "Status-based action items for the user",
            "items": {
              "type": "object",
              "properties": {
                "type": {
                  "type": "string",
                  "enum": [
                    "begin_review",
                    "complete_self_assessment",
                    "awaiting_approval"
                  ]
                },
                "text": {
                  "type": "string"
                },
                "priority": {
                  "type": "string",
                  "enum": [
                    "high",
                    "medium",
                    "low"
                  ]
                }
              }
            }
          }
        }
      },
      "EPMSContinuousFeedback": {
        "type": "object",
        "description": "Continuous feedback record",
        "properties": {
          "id": {
            "type": "integer",
            "example": 345
          },
          "subject": {
            "type": "string",
            "example": "Great work on the project"
          },
          "content": {
            "type": "string",
            "example": "Your presentation was excellent and well-received by the team."
          },
          "feedback_type": {
            "type": "string",
            "enum": [
              "praise",
              "constructive",
              "goal_progress",
              "general"
            ],
            "example": "praise"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "sent",
              "acknowledged",
              "archived"
            ],
            "example": "sent"
          },
          "visibility": {
            "type": "string",
            "enum": [
              "private",
              "public",
              "manager_only"
            ],
            "example": "private"
          },
          "is_anonymous": {
            "type": "boolean",
            "example": false
          },
          "giver": {
            "type": "object",
            "nullable": true,
            "description": "Feedback giver info. Returns only `{ name: \"Anonymous\" }` when feedback is anonymous.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 789
              },
              "name": {
                "type": "string",
                "example": "Jane Manager"
              },
              "email": {
                "type": "string",
                "format": "email",
                "example": "jane.manager@company.com"
              },
              "job_title": {
                "type": "string",
                "nullable": true,
                "example": "Engineering Manager"
              },
              "profile_photo_url": {
                "type": "string",
                "format": "uri",
                "nullable": true,
                "description": "Full-size profile photo URL (200x200)",
                "example": "https://example.com/photos/jane-200x200.jpg"
              },
              "profile_photo_thumbnail_url": {
                "type": "string",
                "format": "uri",
                "nullable": true,
                "description": "Thumbnail profile photo URL (40x40)",
                "example": "https://example.com/photos/jane-40x40.jpg"
              }
            }
          },
          "receiver": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "example": 456
              },
              "name": {
                "type": "string",
                "example": "John Doe"
              },
              "email": {
                "type": "string",
                "format": "email",
                "example": "john.doe@company.com"
              },
              "job_title": {
                "type": "string",
                "nullable": true,
                "example": "Senior Product Manager"
              },
              "profile_photo_url": {
                "type": "string",
                "format": "uri",
                "nullable": true,
                "description": "Full-size profile photo URL (200x200)",
                "example": "https://example.com/photos/john-200x200.jpg"
              },
              "profile_photo_thumbnail_url": {
                "type": "string",
                "format": "uri",
                "nullable": true,
                "description": "Thumbnail profile photo URL (40x40)",
                "example": "https://example.com/photos/john-40x40.jpg"
              }
            }
          },
          "acknowledged_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": "2026-01-20T10:00:00Z"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-01-15T14:30:00Z"
          },
          "sent_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": "2026-01-15T15:00:00Z"
          }
        }
      },
      "EPMSContinuousFeedbackInput": {
        "type": "object",
        "description": "Input schema for creating/updating feedback",
        "required": [
          "receiver_id",
          "subject",
          "content"
        ],
        "properties": {
          "receiver_id": {
            "type": "integer",
            "example": 456
          },
          "subject": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255,
            "example": "Great work on the project"
          },
          "content": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2000,
            "example": "Your presentation was excellent and well-received by the team."
          },
          "feedback_type": {
            "type": "string",
            "enum": [
              "praise",
              "constructive",
              "goal_progress",
              "general"
            ],
            "default": "praise",
            "example": "praise"
          },
          "visibility": {
            "type": "string",
            "enum": [
              "private",
              "public",
              "manager_only"
            ],
            "default": "private",
            "example": "private"
          },
          "is_anonymous": {
            "type": "boolean",
            "default": false,
            "example": false
          }
        }
      },
      "EPMSContinuousFeedbackStats": {
        "type": "object",
        "description": "Feedback statistics",
        "properties": {
          "given": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer",
                "example": 25
              },
              "by_type": {
                "type": "object",
                "properties": {
                  "praise": {
                    "type": "integer",
                    "example": 15
                  },
                  "constructive": {
                    "type": "integer",
                    "example": 5
                  },
                  "recognition": {
                    "type": "integer",
                    "example": 3
                  },
                  "coaching": {
                    "type": "integer",
                    "example": 2
                  }
                }
              }
            }
          },
          "received": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer",
                "example": 18
              },
              "pending_acknowledgment": {
                "type": "integer",
                "example": 3
              },
              "acknowledged": {
                "type": "integer",
                "example": 15
              }
            }
          }
        }
      },
      "EPMSFeedbackPickerUser": {
        "type": "object",
        "description": "Lightweight user object returned by the feedback user-picker endpoint.\nUsed for selecting recipients (giving mode) or viewing feedback targets (receiving mode).\n",
        "properties": {
          "id": {
            "type": "integer",
            "description": "User's internal ID",
            "example": 456
          },
          "name": {
            "type": "string",
            "description": "User's full name",
            "example": "Jane Smith"
          },
          "email": {
            "type": "string",
            "format": "email",
            "nullable": true,
            "description": "User's email address",
            "example": "jane.smith@company.com"
          },
          "job_title": {
            "type": "string",
            "nullable": true,
            "description": "User's job title",
            "example": "Senior Engineer"
          },
          "department": {
            "type": "string",
            "nullable": true,
            "description": "User's department name",
            "example": "Engineering"
          },
          "avatar_url": {
            "type": "string",
            "format": "uri",
            "nullable": true,
            "description": "Full-size profile photo URL (200×200 px)",
            "example": "https://example.com/photos/jane-200x200.jpg"
          },
          "avatar_thumbnail_url": {
            "type": "string",
            "format": "uri",
            "nullable": true,
            "description": "Thumbnail profile photo URL (40×40 px)",
            "example": "https://example.com/photos/jane-40x40.jpg"
          }
        },
        "required": [
          "id",
          "name"
        ]
      },
      "EPMSDevelopmentPlan": {
        "type": "object",
        "description": "Employee development plan",
        "properties": {
          "id": {
            "type": "integer",
            "example": 456
          },
          "title": {
            "type": "string",
            "example": "Leadership Development Plan"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "example": "Focus on developing leadership skills"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "active",
              "completed",
              "cancelled"
            ],
            "example": "active"
          },
          "start_date": {
            "type": "string",
            "format": "date",
            "example": "2026-01-01"
          },
          "target_completion_date": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "example": "2026-12-31"
          },
          "plan_type": {
            "type": "string",
            "nullable": true,
            "example": "leadership"
          },
          "employee": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "example": 456
              },
              "name": {
                "type": "string",
                "example": "John Doe"
              }
            }
          },
          "manager": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer",
                "example": 789
              },
              "name": {
                "type": "string",
                "example": "Jane Manager"
              }
            }
          },
          "goals": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EPMSGoal"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-01-01T10:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-01-15T14:30:00Z"
          }
        }
      },
      "EPMSDevelopmentPlanInput": {
        "type": "object",
        "description": "Input schema for creating/updating development plans",
        "required": [
          "title"
        ],
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255,
            "example": "Leadership Development Plan"
          },
          "description": {
            "type": "string",
            "maxLength": 2000,
            "example": "Focus on developing leadership skills"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "active",
              "completed",
              "cancelled"
            ],
            "example": "active"
          },
          "start_date": {
            "type": "string",
            "format": "date",
            "example": "2026-01-01"
          },
          "target_completion_date": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "example": "2026-12-31"
          },
          "employee_id": {
            "type": "integer",
            "example": 456
          },
          "plan_type": {
            "type": "string",
            "example": "leadership"
          }
        }
      },
      "EPMSDevelopmentGoalInput": {
        "type": "object",
        "description": "Input schema for development plan goals",
        "required": [
          "title",
          "target_date"
        ],
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255,
            "example": "Complete Leadership Training"
          },
          "description": {
            "type": "string",
            "maxLength": 2000,
            "example": "Finish the leadership certification program"
          },
          "goal_type": {
            "type": "string",
            "enum": [
              "skill",
              "experience",
              "education",
              "certification",
              "project"
            ],
            "example": "skill"
          },
          "status": {
            "type": "string",
            "enum": [
              "not_started",
              "in_progress",
              "completed",
              "cancelled"
            ],
            "example": "not_started"
          },
          "target_date": {
            "type": "string",
            "format": "date",
            "example": "2026-06-30"
          },
          "progress_percentage": {
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "default": 0,
            "example": 0
          },
          "priority": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high",
              "critical"
            ],
            "example": "high"
          }
        }
      },
      "EPMSMeeting": {
        "type": "object",
        "description": "One-on-one meeting or check-in",
        "properties": {
          "id": {
            "type": "integer",
            "example": 567
          },
          "title": {
            "type": "string",
            "example": "Q1 Check-in"
          },
          "meeting_type": {
            "type": "string",
            "enum": [
              "performance_review",
              "goal_discussion",
              "feedback_session",
              "check_in",
              "development_planning",
              "career_discussion"
            ],
            "example": "check_in"
          },
          "status": {
            "type": "string",
            "enum": [
              "scheduled",
              "confirmed",
              "in_progress",
              "completed",
              "cancelled",
              "rescheduled"
            ],
            "example": "scheduled"
          },
          "scheduled_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-02-15T10:00:00Z"
          },
          "duration_minutes": {
            "type": "integer",
            "example": 30
          },
          "location": {
            "type": "string",
            "nullable": true,
            "example": "Conference Room A"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "example": "Quarterly performance check-in"
          },
          "employee": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "example": 456
              },
              "name": {
                "type": "string",
                "example": "John Doe"
              }
            }
          },
          "manager": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer",
                "example": 789
              },
              "name": {
                "type": "string",
                "example": "Jane Manager"
              }
            }
          },
          "notes": {
            "type": "string",
            "nullable": true,
            "example": "Great discussion about goals and progress"
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": "2026-02-15T10:30:00Z"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-01-15T10:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-01-15T10:00:00Z"
          }
        }
      },
      "EPMSMeetingInput": {
        "type": "object",
        "description": "Input schema for creating/updating meetings",
        "required": [
          "title",
          "meeting_type",
          "employee_id",
          "scheduled_at"
        ],
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255,
            "example": "Q1 Check-in"
          },
          "meeting_type": {
            "type": "string",
            "enum": [
              "performance_review",
              "goal_discussion",
              "feedback_session",
              "check_in",
              "development_planning",
              "career_discussion"
            ],
            "example": "check_in"
          },
          "employee_id": {
            "type": "integer",
            "example": 456
          },
          "scheduled_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-02-15T10:00:00Z"
          },
          "duration_minutes": {
            "type": "integer",
            "default": 30,
            "example": 30
          },
          "location": {
            "type": "string",
            "maxLength": 255,
            "example": "Conference Room A"
          },
          "description": {
            "type": "string",
            "maxLength": 2000,
            "example": "Quarterly performance check-in"
          }
        }
      },
      "EPMSCompetencyFramework": {
        "type": "object",
        "description": "Competency framework with defined competencies",
        "properties": {
          "id": {
            "type": "integer",
            "example": 678
          },
          "name": {
            "type": "string",
            "example": "Leadership Competencies"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "example": "Core leadership competencies for management roles"
          },
          "framework_type": {
            "type": "string",
            "enum": [
              "core",
              "leadership",
              "technical",
              "functional",
              "role_specific"
            ],
            "example": "leadership"
          },
          "competencies": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 1
                },
                "name": {
                  "type": "string",
                  "example": "Communication"
                },
                "description": {
                  "type": "string",
                  "nullable": true,
                  "example": "Ability to communicate effectively"
                },
                "level": {
                  "type": "string",
                  "enum": [
                    "beginner",
                    "intermediate",
                    "advanced",
                    "expert"
                  ],
                  "nullable": true
                }
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-01-01T10:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-01-01T10:00:00Z"
          }
        }
      },
      "EPMSCompetencyAssessment": {
        "type": "object",
        "description": "Competency assessment with ratings",
        "properties": {
          "id": {
            "type": "integer",
            "example": 789
          },
          "employee": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "example": 456
              },
              "name": {
                "type": "string",
                "example": "John Doe"
              }
            }
          },
          "competency_framework": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "example": 678
              },
              "name": {
                "type": "string",
                "example": "Leadership Competencies"
              }
            }
          },
          "assessment_type": {
            "type": "string",
            "enum": [
              "self_assessment",
              "manager_assessment",
              "peer_assessment",
              "360_assessment"
            ],
            "example": "manager_assessment"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "in_progress",
              "completed"
            ],
            "example": "completed"
          },
          "assessed_at": {
            "type": "string",
            "format": "date",
            "example": "2026-01-15"
          },
          "due_date": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "example": "2026-01-31"
          },
          "overall_score": {
            "type": "number",
            "nullable": true,
            "minimum": 0,
            "maximum": 5,
            "example": 4.2
          },
          "overall_comments": {
            "type": "string",
            "nullable": true,
            "example": "Strong performance across all competencies"
          },
          "competency_ratings": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "competency_id": {
                  "type": "integer",
                  "example": 1
                },
                "competency_name": {
                  "type": "string",
                  "example": "Communication"
                },
                "rating_value": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 5,
                  "example": 4.5
                },
                "comments": {
                  "type": "string",
                  "nullable": true,
                  "example": "Excellent communication skills"
                }
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-01-15T10:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-01-15T14:30:00Z"
          }
        }
      },
      "EPMSCompetencyAssessmentInput": {
        "type": "object",
        "description": "Input schema for creating/updating competency assessments",
        "required": [
          "employee_id",
          "competency_framework_id"
        ],
        "properties": {
          "employee_id": {
            "type": "integer",
            "example": 456
          },
          "competency_framework_id": {
            "type": "integer",
            "example": 678
          },
          "assessment_type": {
            "type": "string",
            "enum": [
              "self_assessment",
              "manager_assessment",
              "peer_assessment",
              "360_assessment"
            ],
            "example": "manager_assessment"
          },
          "assessed_at": {
            "type": "string",
            "format": "date",
            "example": "2026-01-15"
          },
          "due_date": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "example": "2026-01-31"
          },
          "overall_score": {
            "type": "number",
            "minimum": 0,
            "maximum": 5,
            "example": 4.2
          },
          "overall_comments": {
            "type": "string",
            "maxLength": 2000,
            "example": "Strong performance across all competencies"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "in_progress",
              "completed"
            ],
            "example": "completed"
          },
          "competency_ratings_attributes": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "competency_id",
                "rating_value"
              ],
              "properties": {
                "competency_id": {
                  "type": "integer",
                  "example": 1
                },
                "rating_value": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 5,
                  "example": 4.5
                },
                "comments": {
                  "type": "string",
                  "maxLength": 1000,
                  "example": "Excellent communication skills"
                }
              }
            }
          }
        }
      },
      "FormSubmission": {
        "type": "object",
        "description": "Form submission data and metadata",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique submission ID",
            "example": 789
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "submitted",
              "under_review",
              "approved",
              "rejected",
              "pending_completion",
              "changes_requested"
            ],
            "description": "Submission status",
            "example": "submitted"
          },
          "completion_percentage": {
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "description": "Form completion percentage",
            "example": 100
          },
          "submission_data": {
            "type": "object",
            "description": "Form field values",
            "additionalProperties": true,
            "example": {
              "incident_type": "injury",
              "description": "Employee slipped on wet floor",
              "severity": "minor"
            }
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time",
            "description": "Submission timestamp",
            "nullable": true,
            "example": "2024-01-15T14:30:00Z"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Creation timestamp",
            "example": "2024-01-15T14:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "Last update timestamp",
            "example": "2024-01-15T14:30:00Z"
          },
          "files": {
            "type": "array",
            "description": "Files uploaded with this submission",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 12
                },
                "field_name": {
                  "type": "string",
                  "description": "Name of the form field the file belongs to",
                  "example": "incident_photo"
                },
                "file_type": {
                  "type": "string",
                  "description": "Detected file type (image, video, audio, pdf, document, unknown)",
                  "example": "image"
                },
                "filename": {
                  "type": "string",
                  "description": "Original filename",
                  "example": "photo.jpg"
                },
                "url": {
                  "type": "string",
                  "description": "URL to download/view the file",
                  "example": "https://example.com/files/12"
                }
              }
            }
          },
          "device_info": {
            "type": "object",
            "description": "Device metadata captured at submission time",
            "additionalProperties": true,
            "nullable": true
          },
          "is_offline_submission": {
            "type": "boolean",
            "description": "Whether the submission was created from offline mode",
            "example": false
          }
        },
        "required": [
          "id",
          "status",
          "submission_data",
          "completion_percentage"
        ]
      },
      "ReviewedSubmission": {
        "allOf": [
          {
            "type": "object",
            "description": "Form submission data and metadata",
            "properties": {
              "id": {
                "type": "integer",
                "description": "Unique submission ID",
                "example": 789
              },
              "status": {
                "type": "string",
                "enum": [
                  "draft",
                  "submitted",
                  "under_review",
                  "approved",
                  "rejected",
                  "pending_completion",
                  "changes_requested"
                ],
                "description": "Submission status",
                "example": "submitted"
              },
              "completion_percentage": {
                "type": "number",
                "minimum": 0,
                "maximum": 100,
                "description": "Form completion percentage",
                "example": 100
              },
              "submission_data": {
                "type": "object",
                "description": "Form field values",
                "additionalProperties": true,
                "example": {
                  "incident_type": "injury",
                  "description": "Employee slipped on wet floor",
                  "severity": "minor"
                }
              },
              "submitted_at": {
                "type": "string",
                "format": "date-time",
                "description": "Submission timestamp",
                "nullable": true,
                "example": "2024-01-15T14:30:00Z"
              },
              "created_at": {
                "type": "string",
                "format": "date-time",
                "description": "Creation timestamp",
                "example": "2024-01-15T14:00:00Z"
              },
              "updated_at": {
                "type": "string",
                "format": "date-time",
                "description": "Last update timestamp",
                "example": "2024-01-15T14:30:00Z"
              },
              "files": {
                "type": "array",
                "description": "Files uploaded with this submission",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer",
                      "example": 12
                    },
                    "field_name": {
                      "type": "string",
                      "description": "Name of the form field the file belongs to",
                      "example": "incident_photo"
                    },
                    "file_type": {
                      "type": "string",
                      "description": "Detected file type (image, video, audio, pdf, document, unknown)",
                      "example": "image"
                    },
                    "filename": {
                      "type": "string",
                      "description": "Original filename",
                      "example": "photo.jpg"
                    },
                    "url": {
                      "type": "string",
                      "description": "URL to download/view the file",
                      "example": "https://example.com/files/12"
                    }
                  }
                }
              },
              "device_info": {
                "type": "object",
                "description": "Device metadata captured at submission time",
                "additionalProperties": true,
                "nullable": true
              },
              "is_offline_submission": {
                "type": "boolean",
                "description": "Whether the submission was created from offline mode",
                "example": false
              }
            },
            "required": [
              "id",
              "status",
              "submission_data",
              "completion_percentage"
            ]
          },
          {
            "type": "object",
            "properties": {
              "status_label": {
                "type": "string",
                "description": "Human-readable status label",
                "example": "Approved"
              },
              "reviewed_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When the review decision was taken",
                "example": "2026-06-24T14:30:00Z"
              },
              "review_round": {
                "type": "integer",
                "description": "Which correction cycle the submission is on. 0 until the first\nfield-level return; incremented by every\n`POST /form_submissions/{id}/return_fields`. Compare against a\nfield's `field_review.round` to tell a current return from a\nhistorical one.\n",
                "example": 2
              },
              "review_notes": {
                "type": "string",
                "nullable": true,
                "description": "Reviewer notes (the rejection reason for reject)",
                "example": "Approved — all required documents attached."
              },
              "reviewed_by": {
                "type": "object",
                "nullable": true,
                "description": "The reviewer who took the decision",
                "properties": {
                  "id": {
                    "type": "integer",
                    "example": 7
                  },
                  "name": {
                    "type": "string",
                    "example": "Anup Patel"
                  }
                }
              }
            }
          }
        ]
      },
      "SubmissionTimelineEvent": {
        "type": "object",
        "description": "A single event on the submission status timeline",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "submitted",
              "approved",
              "changes_requested",
              "rejected"
            ],
            "description": "Machine-readable event kind"
          },
          "title": {
            "type": "string",
            "description": "Human-readable event title",
            "example": "Rejected"
          },
          "state": {
            "type": "string",
            "enum": [
              "done",
              "warning",
              "bad"
            ],
            "description": "Visual marker state — done (success/neutral), warning (returned for correction), bad (rejected)"
          },
          "at": {
            "type": "string",
            "format": "date-time",
            "description": "When the event occurred"
          },
          "by": {
            "type": "object",
            "nullable": true,
            "description": "The reviewer who performed the review event; null for the submitted event",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string",
                "example": "Reva Yu"
              }
            }
          },
          "note": {
            "type": "string",
            "nullable": true,
            "description": "Reviewer's note for the outcome (e.g. rejection reason); null when none",
            "example": "Please retake photo 2."
          }
        },
        "required": [
          "type",
          "title",
          "state",
          "at"
        ]
      },
      "LeaveRequest": {
        "type": "object",
        "description": "Basic leave request information",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique leave request ID",
            "example": 123
          },
          "user_id": {
            "type": "integer",
            "description": "ID of the user who created the request",
            "example": 456
          },
          "leave_type": {
            "$ref": "#/components/schemas/LeaveTypeBasic"
          },
          "start_date": {
            "type": "string",
            "format": "date",
            "description": "Start date of leave",
            "example": "2024-03-15"
          },
          "end_date": {
            "type": "string",
            "format": "date",
            "description": "End date of leave",
            "example": "2024-03-17"
          },
          "hours_calculated": {
            "type": "number",
            "description": "Total hours for this leave request (primary field)",
            "example": 24
          },
          "business_days": {
            "type": "number",
            "description": "DEPRECATED: Use hours_calculated instead. Returns same value as hours_calculated for backward compatibility.",
            "example": 24
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "approved",
              "denied",
              "cancelled",
              "special_approval"
            ],
            "description": "Current status of the leave request",
            "example": "pending"
          },
          "notes": {
            "type": "string",
            "nullable": true,
            "description": "Optional notes for the leave request",
            "example": "Family vacation"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the request was created",
            "example": "2024-02-15T10:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the request was last updated",
            "example": "2024-02-15T10:00:00Z"
          }
        },
        "required": [
          "id",
          "user_id",
          "leave_type",
          "start_date",
          "end_date",
          "hours_calculated",
          "status",
          "created_at",
          "updated_at"
        ]
      },
      "LeaveRequestDetailed": {
        "allOf": [
          {
            "$ref": "#/components/schemas/LeaveRequest"
          },
          {
            "type": "object",
            "properties": {
              "approval_details": {
                "type": "object",
                "description": "Approval workflow information",
                "properties": {
                  "requires_approval": {
                    "type": "boolean",
                    "description": "Whether this request requires approval",
                    "example": true
                  },
                  "approved_by": {
                    "type": "object",
                    "nullable": true,
                    "description": "User who approved the request",
                    "properties": {
                      "id": {
                        "type": "integer",
                        "example": 789
                      },
                      "name": {
                        "type": "string",
                        "example": "Jane Manager"
                      }
                    }
                  },
                  "approved_at": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true,
                    "description": "When the request was approved",
                    "example": "2024-02-16T14:30:00Z"
                  },
                  "denial_reason": {
                    "type": "string",
                    "nullable": true,
                    "description": "Reason for denial if applicable",
                    "example": "Insufficient coverage during requested period"
                  }
                }
              },
              "conflicts": {
                "type": "object",
                "description": "Shift conflict information",
                "properties": {
                  "has_conflicts": {
                    "type": "boolean",
                    "description": "Whether this request conflicts with assigned shifts",
                    "example": false
                  },
                  "conflicting_shifts": {
                    "type": "array",
                    "description": "List of conflicting shifts",
                    "items": {
                      "$ref": "#/components/schemas/ShiftConflict"
                    }
                  }
                }
              },
              "blackout_warnings": {
                "type": "array",
                "description": "Blackout period warnings",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer",
                      "example": 1
                    },
                    "name": {
                      "type": "string",
                      "example": "Winter Holiday Season"
                    },
                    "start_date": {
                      "type": "string",
                      "format": "date",
                      "example": "2024-12-15"
                    },
                    "end_date": {
                      "type": "string",
                      "format": "date",
                      "example": "2024-12-31"
                    },
                    "blocks_requests": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Leave requests are blocked during Winter Holiday Season"
                    }
                  }
                }
              },
              "coverage_impact": {
                "type": "object",
                "nullable": true,
                "description": "Coverage impact analysis",
                "properties": {
                  "affects_coverage": {
                    "type": "boolean",
                    "example": false
                  },
                  "coverage_percentage": {
                    "type": "number",
                    "example": 85.5
                  },
                  "available_spots": {
                    "type": "integer",
                    "example": 3
                  },
                  "message": {
                    "type": "string",
                    "example": "This request will not affect coverage limits"
                  }
                }
              }
            }
          }
        ]
      },
      "LeaveBalance": {
        "type": "object",
        "description": "Leave balance information for a specific leave type. All numeric values are in HOURS.",
        "properties": {
          "leave_type_id": {
            "type": "integer",
            "description": "ID of the leave type",
            "example": 1
          },
          "leave_type": {
            "$ref": "#/components/schemas/LeaveTypeBasic"
          },
          "year": {
            "type": "integer",
            "description": "Year for this balance",
            "example": 2024
          },
          "unit": {
            "type": "string",
            "description": "Unit of measurement for balance values",
            "example": "hours",
            "enum": [
              "hours"
            ]
          },
          "accrued": {
            "type": "number",
            "description": "Total hours accrued for the year",
            "example": 120.0
          },
          "used": {
            "type": "number",
            "description": "Hours already used",
            "example": 40.0
          },
          "balance": {
            "type": "number",
            "description": "Available hours remaining",
            "example": 80.0
          },
          "pending": {
            "type": "number",
            "description": "Hours in pending requests",
            "example": 24.0
          },
          "percentages": {
            "type": "object",
            "description": "Usage percentages for progress indicators",
            "properties": {
              "used": {
                "type": "number",
                "description": "Percentage of accrued hours used",
                "example": 33.3
              },
              "pending": {
                "type": "number",
                "description": "Percentage of accrued hours pending",
                "example": 20.0
              },
              "available": {
                "type": "number",
                "description": "Percentage of accrued hours available",
                "example": 46.7
              }
            }
          }
        },
        "required": [
          "leave_type_id",
          "leave_type",
          "year",
          "accrued",
          "used",
          "balance",
          "pending",
          "percentages"
        ]
      },
      "LeaveBalanceDetailed": {
        "allOf": [
          {
            "$ref": "#/components/schemas/LeaveBalance"
          },
          {
            "type": "object",
            "properties": {
              "recent_usage": {
                "type": "array",
                "description": "Leave consumed against this balance in the last 6 months, newest first. Includes approved leave requests, usage an administrator recorded by hand, and hours paid out from banked time — all three move the balance's `used` figure. Pending requests are NOT included (they are not usage yet); read the balance's `pending` hours for those.",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer",
                      "description": "Identifier within the table named by `record_type`.",
                      "example": 123
                    },
                    "record_type": {
                      "type": "string",
                      "description": "Which record `id` refers to. `leave_request` for an approved request; `leave_ledger_entry` for administrator- recorded usage or a banked-time payout.",
                      "enum": [
                        "leave_request",
                        "leave_ledger_entry"
                      ],
                      "example": "leave_request"
                    },
                    "source": {
                      "type": "string",
                      "enum": [
                        "leave_request",
                        "manual",
                        "payout"
                      ],
                      "example": "leave_request"
                    },
                    "start_date": {
                      "type": "string",
                      "format": "date",
                      "example": "2024-01-15"
                    },
                    "end_date": {
                      "type": "string",
                      "format": "date",
                      "example": "2024-01-17"
                    },
                    "hours": {
                      "type": "number",
                      "description": "Hours actually charged to the balance (the paid portion). Use this figure to reconcile against `used`.",
                      "example": 16
                    },
                    "unpaid_hours": {
                      "type": "number",
                      "description": "Portion taken as unpaid leave, which never touches the balance.",
                      "example": 8
                    },
                    "hours_calculated": {
                      "type": "number",
                      "description": "Total hours on the request, including any unpaid portion. Equals `hours` unless the request has an unpaid split.",
                      "example": 24
                    },
                    "business_days": {
                      "type": "number",
                      "description": "DEPRECATED: Use hours_calculated",
                      "example": 24
                    },
                    "status": {
                      "type": "string",
                      "nullable": true,
                      "description": "Always `approved` for a leave request; null for ledger entries.",
                      "example": "approved"
                    },
                    "upcoming": {
                      "type": "boolean",
                      "description": "Approved but the time off has not happened yet.",
                      "example": false
                    },
                    "counts_toward_balance": {
                      "type": "boolean",
                      "description": "False for a historical record imported alongside the balance figures its hours are already inside. Such rows are listed but excluded from `used`.",
                      "example": true
                    },
                    "notes": {
                      "type": "string",
                      "nullable": true,
                      "example": "Vacation"
                    },
                    "edited": {
                      "type": "boolean",
                      "description": "The leave request has been modified since it was submitted.",
                      "example": false
                    },
                    "edit_count": {
                      "type": "integer",
                      "example": 0
                    },
                    "last_edited_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true,
                      "example": "2024-02-01T09:30:00Z"
                    }
                  }
                }
              },
              "accrual_policy": {
                "type": "object",
                "nullable": true,
                "description": "Accrual policy information",
                "properties": {
                  "id": {
                    "type": "integer",
                    "example": 1
                  },
                  "name": {
                    "type": "string",
                    "example": "Standard Vacation Policy"
                  },
                  "accrual_rate": {
                    "type": "number",
                    "description": "Hours accrued per period",
                    "example": 3.08
                  },
                  "accrual_unit": {
                    "type": "string",
                    "description": "Unit for all accrual values",
                    "example": "hours",
                    "enum": [
                      "hours"
                    ]
                  },
                  "accrual_frequency": {
                    "type": "string",
                    "example": "bi-weekly"
                  },
                  "max_balance": {
                    "type": "number",
                    "description": "Maximum balance cap in hours",
                    "nullable": true,
                    "example": 120.0
                  },
                  "max_accrual": {
                    "type": "number",
                    "description": "Deprecated, use max_balance",
                    "nullable": true,
                    "example": 120.0
                  },
                  "carryover_allowed": {
                    "type": "boolean",
                    "example": true
                  },
                  "carryover_max": {
                    "type": "number",
                    "description": "Maximum hours to carry over",
                    "nullable": true,
                    "example": 40.0
                  },
                  "waiting_period_days": {
                    "type": "integer",
                    "description": "Calendar days before accrual starts",
                    "example": 90
                  },
                  "proration_enabled": {
                    "type": "boolean",
                    "example": true
                  }
                }
              },
              "projected_balance": {
                "type": "number",
                "description": "Projected balance (hours) at end of year",
                "example": 100.0
              }
            }
          }
        ]
      },
      "LeaveBalanceSummary": {
        "type": "object",
        "description": "Comprehensive leave balance summary",
        "properties": {
          "totals": {
            "type": "object",
            "description": "Totals across all leave types",
            "properties": {
              "accrued": {
                "type": "number",
                "example": 25.0
              },
              "used": {
                "type": "number",
                "example": 8.0
              },
              "pending": {
                "type": "number",
                "example": 3.0
              },
              "available": {
                "type": "number",
                "example": 14.0
              },
              "usage_percentage": {
                "type": "number",
                "example": 32.0
              }
            }
          },
          "by_leave_type": {
            "type": "array",
            "description": "Balances by leave type",
            "items": {
              "$ref": "#/components/schemas/LeaveBalance"
            }
          },
          "upcoming_leave": {
            "type": "array",
            "description": "Upcoming approved leave",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 123
                },
                "leave_type": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer",
                      "example": 1
                    },
                    "name": {
                      "type": "string",
                      "example": "Vacation"
                    },
                    "color": {
                      "type": "string",
                      "example": "#4CAF50"
                    }
                  }
                },
                "start_date": {
                  "type": "string",
                  "format": "date",
                  "example": "2024-04-15"
                },
                "end_date": {
                  "type": "string",
                  "format": "date",
                  "example": "2024-04-17"
                },
                "hours": {
                  "type": "number",
                  "description": "Hours that will be charged to the balance (the paid portion).",
                  "example": 16
                },
                "unpaid_hours": {
                  "type": "number",
                  "description": "Portion to be taken as unpaid leave, which never touches the balance.",
                  "example": 8
                },
                "hours_calculated": {
                  "type": "number",
                  "description": "Total hours on the request, including any unpaid portion. Equals `hours` unless the request has an unpaid split.",
                  "example": 24
                },
                "business_days": {
                  "type": "number",
                  "description": "DEPRECATED: Use hours_calculated",
                  "example": 24
                },
                "notes": {
                  "type": "string",
                  "nullable": true,
                  "example": "Spring vacation"
                }
              }
            }
          },
          "year": {
            "type": "integer",
            "description": "Year for this summary",
            "example": 2024
          }
        },
        "required": [
          "totals",
          "by_leave_type",
          "upcoming_leave",
          "year"
        ]
      },
      "LeaveType": {
        "type": "object",
        "description": "Leave type information",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique leave type ID",
            "example": 1
          },
          "name": {
            "type": "string",
            "description": "Name of the leave type",
            "example": "Vacation"
          },
          "color": {
            "type": "string",
            "description": "Color code for UI display",
            "example": "#4CAF50"
          },
          "icon": {
            "type": "string",
            "description": "Icon class for UI display",
            "example": "fas fa-calendar-star"
          },
          "active": {
            "type": "boolean",
            "description": "Whether this leave type is active",
            "example": true
          },
          "default": {
            "type": "boolean",
            "description": "Whether this is the default leave type",
            "example": false
          },
          "current_balance": {
            "type": "object",
            "nullable": true,
            "description": "Current balance for this leave type (if requested)",
            "properties": {
              "accrued": {
                "type": "number",
                "example": 15.0
              },
              "used": {
                "type": "number",
                "example": 5.0
              },
              "balance": {
                "type": "number",
                "example": 10.0
              },
              "year": {
                "type": "integer",
                "example": 2024
              }
            }
          }
        },
        "required": [
          "id",
          "name",
          "color",
          "icon",
          "active",
          "default"
        ]
      },
      "UserAvailability": {
        "type": "object",
        "description": "User availability block information",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique availability ID",
            "example": 123
          },
          "specific_date": {
            "type": "string",
            "format": "date",
            "description": "Date for this availability block",
            "example": "2025-10-15"
          },
          "start_time": {
            "type": "string",
            "format": "time",
            "description": "Start time (displayed in user's timezone)",
            "example": "09:00"
          },
          "end_time": {
            "type": "string",
            "format": "time",
            "description": "End time (displayed in user's timezone)",
            "example": "17:00"
          },
          "notes": {
            "type": "string",
            "nullable": true,
            "description": "Optional notes about this availability",
            "example": "Available for morning shift"
          },
          "store_in_utc": {
            "type": "boolean",
            "description": "Whether times are stored in UTC",
            "example": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When this availability was created",
            "example": "2025-10-01T10:30:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When this availability was last updated",
            "example": "2025-10-01T10:30:00Z"
          }
        },
        "required": [
          "id",
          "specific_date",
          "start_time",
          "end_time"
        ]
      },
      "WeeklyAvailabilityConfirmation": {
        "type": "object",
        "description": "Weekly availability confirmation information",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique confirmation ID",
            "example": 456
          },
          "start_date": {
            "type": "string",
            "format": "date",
            "description": "Week start date",
            "example": "2025-10-13"
          },
          "end_date": {
            "type": "string",
            "format": "date",
            "description": "Week end date",
            "example": "2025-10-19"
          },
          "confirmed_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the availability was confirmed",
            "example": "2025-10-12T15:30:00Z"
          },
          "notes": {
            "type": "string",
            "nullable": true,
            "description": "Optional notes about the confirmation",
            "example": "Confirmed for this week"
          },
          "auto_generated": {
            "type": "boolean",
            "description": "Whether this was auto-generated by the system",
            "example": false
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When this confirmation was created",
            "example": "2025-10-12T15:30:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When this confirmation was last updated",
            "example": "2025-10-12T15:30:00Z"
          }
        },
        "required": [
          "id",
          "start_date",
          "end_date",
          "confirmed_at",
          "auto_generated"
        ]
      },
      "LeaveTypeBasic": {
        "type": "object",
        "description": "Basic leave type information for references",
        "properties": {
          "id": {
            "type": "integer",
            "example": 1
          },
          "name": {
            "type": "string",
            "example": "Vacation"
          },
          "color": {
            "type": "string",
            "example": "#4CAF50"
          },
          "icon": {
            "type": "string",
            "example": "fas fa-calendar-star"
          }
        },
        "required": [
          "id",
          "name",
          "color",
          "icon"
        ]
      },
      "LeaveTypeDetailed": {
        "allOf": [
          {
            "$ref": "#/components/schemas/LeaveType"
          },
          {
            "type": "object",
            "properties": {
              "description": {
                "type": "string",
                "nullable": true,
                "description": "Description of the leave type",
                "example": "Annual vacation leave for rest and recreation"
              },
              "advance_notice_days": {
                "type": "integer",
                "description": "Required advance notice in days",
                "example": 7
              },
              "documentation_required": {
                "type": "boolean",
                "description": "Whether documentation is required",
                "example": false
              },
              "documentation_threshold_days": {
                "type": "integer",
                "description": "Threshold for requiring documentation",
                "example": 3
              },
              "requires_approval": {
                "type": "boolean",
                "description": "Whether requests require approval",
                "example": true
              },
              "deduct_from_balance": {
                "type": "boolean",
                "description": "Whether to deduct from balance",
                "example": true
              },
              "allow_negative_balance": {
                "type": "boolean",
                "description": "Whether negative balances are allowed",
                "example": false
              },
              "accrual_enabled": {
                "type": "boolean",
                "description": "Whether accrual is enabled",
                "example": true
              },
              "accrual_policy": {
                "type": "object",
                "nullable": true,
                "description": "Associated accrual policy",
                "properties": {
                  "id": {
                    "type": "integer",
                    "example": 1
                  },
                  "name": {
                    "type": "string",
                    "example": "Standard Vacation Policy"
                  },
                  "accrual_rate": {
                    "type": "number",
                    "example": 1.25
                  },
                  "accrual_frequency": {
                    "type": "string",
                    "example": "monthly"
                  },
                  "accrual_frequency_display": {
                    "type": "string",
                    "example": "Monthly"
                  },
                  "max_accrual": {
                    "type": "number",
                    "nullable": true,
                    "example": 30.0
                  },
                  "carryover_allowed": {
                    "type": "boolean",
                    "example": true
                  },
                  "carryover_max": {
                    "type": "number",
                    "nullable": true,
                    "example": 5.0
                  },
                  "waiting_period_days": {
                    "type": "integer",
                    "example": 90
                  },
                  "proration_enabled": {
                    "type": "boolean",
                    "example": true
                  }
                }
              },
              "business_rules": {
                "type": "object",
                "description": "Business rules and policies",
                "properties": {
                  "advance_notice_required": {
                    "type": "boolean",
                    "example": true
                  },
                  "documentation_rules": {
                    "type": "object",
                    "properties": {
                      "required": {
                        "type": "boolean",
                        "example": false
                      },
                      "threshold_days": {
                        "type": "integer",
                        "example": 3
                      }
                    }
                  },
                  "approval_workflow": {
                    "type": "object",
                    "properties": {
                      "requires_approval": {
                        "type": "boolean",
                        "example": true
                      },
                      "auto_approve_threshold": {
                        "type": "number",
                        "nullable": true,
                        "example": null
                      }
                    }
                  },
                  "balance_rules": {
                    "type": "object",
                    "properties": {
                      "deduct_from_balance": {
                        "type": "boolean",
                        "example": true
                      },
                      "allow_negative": {
                        "type": "boolean",
                        "example": false
                      }
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "ShiftConflict": {
        "type": "object",
        "description": "Information about a conflicting shift",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Shift ID",
            "example": 789
          },
          "name": {
            "type": "string",
            "description": "Shift name",
            "example": "Morning Shift"
          },
          "start_time": {
            "type": "string",
            "format": "date-time",
            "description": "Shift start time",
            "example": "2024-03-15T08:00:00Z"
          },
          "end_time": {
            "type": "string",
            "format": "date-time",
            "description": "Shift end time",
            "example": "2024-03-15T16:00:00Z"
          },
          "location": {
            "type": "object",
            "nullable": true,
            "description": "Shift location",
            "properties": {
              "id": {
                "type": "integer",
                "example": 1
              },
              "name": {
                "type": "string",
                "example": "Main Office"
              }
            }
          }
        },
        "required": [
          "id",
          "name",
          "start_time",
          "end_time"
        ]
      },
      "PaginationMeta": {
        "type": "object",
        "description": "Pagination metadata",
        "properties": {
          "total_count": {
            "type": "integer",
            "description": "Total number of items",
            "example": 150
          },
          "total_pages": {
            "type": "integer",
            "description": "Total number of pages",
            "example": 6
          },
          "current_page": {
            "type": "integer",
            "description": "Current page number",
            "example": 1
          },
          "per_page": {
            "type": "integer",
            "description": "Items per page",
            "example": 25
          }
        },
        "required": [
          "total_count",
          "total_pages",
          "current_page",
          "per_page"
        ]
      },
      "FormSummaryItem": {
        "type": "object",
        "description": "A published form as returned by GET /api/v1/forms",
        "properties": {
          "id": {
            "type": "integer",
            "example": 42
          },
          "name": {
            "type": "string",
            "example": "Daily Safety Checklist"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "example": "Complete before each shift."
          },
          "category": {
            "type": "string",
            "example": "safety"
          },
          "priority": {
            "type": "string",
            "enum": [
              "urgent",
              "high",
              "normal",
              "low"
            ],
            "example": "high"
          },
          "field_count": {
            "type": "integer",
            "example": 12
          },
          "required_field_count": {
            "type": "integer",
            "example": 8
          },
          "page_count": {
            "type": "integer",
            "description": "Number of pages in the form (page_break fields + 1)",
            "example": 3
          },
          "estimated_time_minutes": {
            "type": "integer",
            "example": 9
          },
          "has_file_uploads": {
            "type": "boolean",
            "example": false
          },
          "sharing_enabled": {
            "type": "boolean",
            "example": true
          },
          "requires_approval": {
            "type": "boolean",
            "example": false
          },
          "accepting_submissions": {
            "type": "boolean",
            "description": "Whether the form is currently open for new responses. `false` when it passed its scheduled close time, reached its response cap, or is not published — exactly the states in which `GET /api/v1/forms/{id}` and `POST /api/v1/forms/{id}/submit` answer 403 `form_not_available` for a user who is not an admin, forms admin, or the form's creator. Badge or disable the row instead of opening it.\n",
            "example": true
          },
          "closed_reason": {
            "type": "string",
            "nullable": true,
            "description": "Short label naming WHY the form is not accepting responses, or `null` when `accepting_submissions` is true. Same vocabulary the web Published Forms list and the mobile cards badge with.\n",
            "enum": [
              "Closed",
              "Response limit reached",
              "Archived",
              "Not published"
            ],
            "example": null
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-01-15T09:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-04-10T14:22:00Z"
          }
        },
        "required": [
          "id",
          "name",
          "category",
          "priority",
          "field_count",
          "required_field_count",
          "page_count",
          "estimated_time_minutes",
          "has_file_uploads",
          "sharing_enabled",
          "requires_approval",
          "accepting_submissions",
          "closed_reason",
          "created_at",
          "updated_at"
        ]
      },
      "FormCategoryItem": {
        "type": "object",
        "description": "A form category as returned by GET /api/v1/forms/categories",
        "properties": {
          "value": {
            "type": "string",
            "description": "Category slug — pass verbatim to GET /api/v1/forms?category=<value>",
            "example": "safety"
          },
          "label": {
            "type": "string",
            "description": "Human-readable category name",
            "example": "Safety"
          },
          "form_count": {
            "type": "integer",
            "description": "Number of published forms in this category for the caller's business",
            "example": 2
          }
        },
        "required": [
          "value",
          "label",
          "form_count"
        ]
      },
      "ApprovalItem": {
        "type": "object",
        "description": "A submission in the reviewer's Approvals queue (GET /api/v1/forms/approvals)",
        "properties": {
          "id": {
            "type": "integer",
            "example": 2231
          },
          "reference": {
            "type": "string",
            "description": "Display reference (\"#<id>\")",
            "example": "#2231"
          },
          "form_id": {
            "type": "integer",
            "example": 7
          },
          "form_name": {
            "type": "string",
            "example": "Site Safety Audit"
          },
          "form_description": {
            "type": "string",
            "nullable": true,
            "example": "Quarterly site-level safety walkthrough"
          },
          "form_category": {
            "type": "string",
            "example": "safety"
          },
          "priority": {
            "type": "string",
            "enum": [
              "urgent",
              "high",
              "normal",
              "low"
            ],
            "description": "Form priority — drives the urgency chip",
            "example": "high"
          },
          "submitter": {
            "type": "string",
            "description": "Submitter's display name (or \"Anonymous User\")",
            "example": "J. Rivera"
          },
          "submitter_email": {
            "type": "string",
            "nullable": true,
            "example": "jrivera@example.com"
          },
          "submitter_photo_url": {
            "type": "string",
            "nullable": true,
            "description": "Thumbnail avatar URL for the submitter; null for anonymous submissions",
            "example": "https://officechat-dev.workforce.mangoapps.com/rails/active_storage/representations/.../avatar.jpg"
          },
          "status": {
            "type": "string",
            "enum": [
              "submitted",
              "under_review"
            ],
            "example": "under_review"
          },
          "status_label": {
            "type": "string",
            "example": "Under review"
          },
          "requires_approval": {
            "type": "boolean",
            "example": true
          },
          "overdue": {
            "type": "boolean",
            "description": "True when this row has been awaiting review longer than the server-side\nreview SLA (currently 7 days on the `waiting_since` clock). Agrees\nrow-for-row with the `?overdue=true` filter — badge from this rather\nthan recomputing the threshold on the client, which would diverge the\nmoment the server policy changed.\n",
            "example": true
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "waiting_since": {
            "type": "string",
            "format": "date-time",
            "description": "When the item entered the queue (submitted_at, falling back to created_at) — use for the \"Waiting Nd\" label"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "reference",
          "form_id",
          "form_name",
          "form_category",
          "priority",
          "submitter",
          "status",
          "status_label",
          "requires_approval",
          "overdue",
          "waiting_since",
          "updated_at",
          "created_at"
        ]
      },
      "MySubmissionItem": {
        "type": "object",
        "description": "A single form submission as returned by GET /api/v1/forms/my_submissions",
        "properties": {
          "id": {
            "type": "integer",
            "example": 1042
          },
          "form_id": {
            "type": "integer",
            "example": 7
          },
          "form_name": {
            "type": "string",
            "description": "Used as the card title in the mobile list",
            "example": "Equipment Transfer"
          },
          "form_description": {
            "type": "string",
            "nullable": true,
            "description": "Used as the card subtitle",
            "example": "Transfer equipment custody between sites"
          },
          "form_category": {
            "type": "string",
            "example": "operations"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "submitted",
              "under_review",
              "approved",
              "rejected",
              "pending_completion",
              "changes_requested"
            ],
            "example": "draft"
          },
          "status_label": {
            "type": "string",
            "description": "Human-readable label for the status badge",
            "example": "Saved draft"
          },
          "completion_percentage": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100,
            "description": "Used for the Progress meta line",
            "example": 85
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "null for drafts",
            "example": null
          },
          "reviewed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When a reviewer approved or rejected; null if not yet reviewed",
            "example": null
          },
          "review_notes": {
            "type": "string",
            "nullable": true,
            "description": "Reviewer's notes on approval or rejection",
            "example": null
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "Used for the \"Last updated\" meta line",
            "example": "2026-06-11T16:30:00Z"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-06-10T08:00:00Z"
          },
          "is_offline_submission": {
            "type": "boolean",
            "example": false
          }
        },
        "required": [
          "id",
          "form_id",
          "form_name",
          "form_category",
          "status",
          "status_label",
          "completion_percentage",
          "updated_at",
          "created_at",
          "is_offline_submission"
        ]
      },
      "NewsFeedSummary": {
        "type": "object",
        "description": "List-card serialization of a feed post.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "content_type": {
            "type": "string",
            "enum": [
              "update",
              "question",
              "poll"
            ]
          },
          "content_category": {
            "type": "string",
            "enum": [
              "operational",
              "social"
            ]
          },
          "source": {
            "type": "object",
            "nullable": true,
            "description": "Provenance for app-contributed posts (`NewsFeed::FeedPublisher.contribute`).\n`null` for ordinary human composer posts.\n\n**This — not `content_type` — is the feed-type discriminator.** A\nBroadcast fans out to its audience and then contributes a feed row\nwith a hardcoded `content_type` of `update`, so a broadcast-originated\npost is indistinguishable from a normal post by `content_type` alone.\nDetect one with `source && source.type == \"Broadcast\"`.\n\nBacked by columns on the feed row (`source_type` / `source_id` /\n`source_event`), so it is N+1-safe on the list path and identical on\nthe list and detail endpoints.\n",
            "properties": {
              "type": {
                "type": "string",
                "description": "Source model name. Known contributors:\n  * `Broadcast`                — a published Broadcast\n  * `CommsHub::Issue`          — a sent newsletter issue\n  * `Livestreaming::LiveEvent` — a livestream recording\n  * `Contests::Contest`        — contest winners announced\n\nTreat the set as open — plugins contribute under their own\nmodel names. Match on the exact string; do not parse it.\n",
                "example": "Broadcast"
              },
              "id": {
                "type": "integer",
                "description": "Primary key of the source record, in that model's own table.",
                "example": 45
              },
              "event": {
                "type": "string",
                "description": "The lifecycle moment that produced this post. Part of the\ncontribution de-dup key, so one source can contribute at most\none feed per event. Known values: `broadcast_published`,\n`issue_published`, `recording_available`, `winners_announced`.\n",
                "example": "broadcast_published"
              },
              "critical": {
                "type": "boolean",
                "description": "Whether the originating record is marked critical (`Broadcast`\nonly today) — the red **CRITICAL** badge. Derived from\nprovenance rather than copied onto the row, so downgrading the\nbroadcast clears the badge and entries published before this\nshipped light up too.\n",
                "example": true
              },
              "channels": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "in_app",
                    "email",
                    "sms",
                    "voice",
                    "push"
                  ]
                },
                "description": "**Broadcast-sourced entries only.** The channels the broadcast\nactually fanned out on — the card's \"Delivered in-app · Push ·\nSMS · Voice\" row. It varies per broadcast (the author's channel\ntoggles), so it cannot be inferred; the feed row carries no\nchannel of its own. `in_app` always delivers; a channel appears\nunless the author switched it off.\n\nAbsent when the post is not broadcast-sourced, or the broadcast\nno longer exists.\n",
                "example": [
                  "in_app",
                  "email",
                  "sms",
                  "push"
                ]
              },
              "requires_acknowledgement": {
                "type": "boolean",
                "description": "**Broadcast-sourced entries only.** Whether the BROADCAST asks\nfor an acknowledgement.\n\nThe post's own top-level `requires_acknowledgement` is\ndeliberately `false` on these entries: the broadcast contributes\nat priority `operational` and never as must-read, so\nacknowledgement stays on ONE ledger rather than being minted a\nsecond time on the post. A client rendering the **Acknowledge**\nbutton therefore reads THIS flag and posts to\n`POST /api/v1/broadcasts/{source.id}/acknowledge` — not the\nfeed's acknowledge endpoint, which records against the wrong\nledger.\n",
                "example": true
              },
              "acknowledged": {
                "type": "boolean",
                "description": "**Broadcast-sourced entries only.** Whether the CALLING user has\nalready acknowledged that broadcast.\n",
                "example": false
              }
            },
            "required": [
              "type",
              "id",
              "event"
            ],
            "example": {
              "type": "Broadcast",
              "id": 617,
              "event": "broadcast_published",
              "critical": true,
              "channels": [
                "in_app",
                "email",
                "sms",
                "push"
              ],
              "requires_acknowledgement": true,
              "acknowledged": false
            }
          },
          "priority": {
            "type": "string",
            "enum": [
              "must_read",
              "operational",
              "social"
            ]
          },
          "is_announcement": {
            "type": "boolean",
            "description": "Whether this post is the composer's **Announcement** kind.\n\nThe Communications composer is one authoring surface with an\nintensity dial, and `priority` is how the chosen kind is stored:\n`social` → post, `operational` → **announcement**, `must_read` →\nmust-read (a Broadcast is its own record, surfaced here via\n`source.type == \"Broadcast\"`). This flag is therefore exactly\n`priority == \"operational\"`, returned as a boolean alongside\n`must_read` so a client can render the Announcement badge without\nhardcoding that priority→kind table. Reading `priority` directly\nstill works and is unchanged.\n\nPresent on the list and detail endpoints alike, for every\n`content_type` (update / question / poll).\n\nNote: a broadcast-contributed post is stored at priority\n`operational` and so reports `true` here — check `source` when you\nneed to tell a broadcast apart from a composed announcement.\n",
            "example": true
          },
          "must_read": {
            "type": "boolean"
          },
          "pinned": {
            "type": "boolean"
          },
          "pinned_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "scheduled",
              "published",
              "expired",
              "archived"
            ]
          },
          "title": {
            "type": "string",
            "nullable": true
          },
          "body": {
            "type": "string",
            "nullable": true
          },
          "published_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "scheduled_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "editable": {
            "type": "boolean",
            "description": "Whether **the calling user** may currently edit this post's content\nand attachments — author, and either the post is draft/scheduled or\nit was published within the last 15 minutes with the admin\n`content_editing` setting on. Lets clients show or hide the edit and\nadd/remove-attachment controls instead of guessing; the server\nre-enforces it on every write regardless.\n"
          },
          "is_edited": {
            "type": "boolean",
            "description": "True once the author edited this post after it was published —\na convenience boolean for `edited_at != null` so clients can render\nan \"edited\" marker without a null check. Applies to any\n`content_type` (update / question / poll).\n"
          },
          "edited_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the post was last edited after publishing. `null` until the\nfirst post-publish edit; drafts and scheduled posts never set it\n(the marker is published-only).\n"
          },
          "audience_type": {
            "type": "string",
            "enum": [
              "everyone",
              "segments"
            ]
          },
          "audience_segment_ids": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "audience_segments": {
            "type": "array",
            "description": "Parallel to `audience_segment_ids` — each segment id resolved to\nits human-readable name so clients can render segment chips\nwithout a second roundtrip. Numeric ids resolve via\n`NotificationRecipientGroup`; the `direct_reports` pseudo-segment\nresolves to \"Author's direct reports\". `name` is `null` when the\nunderlying group has been deleted.\n",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string",
                  "nullable": true
                }
              }
            }
          },
          "audience_size": {
            "type": "integer",
            "nullable": true,
            "minimum": 0,
            "description": "Total addressable audience captured at publish time\n(PostAudience.snapshot_for!). Pair with `read_count` /\n`acknowledged_count` for \"N of M\" badges. `null` for feeds\nthat predate the audience snapshot column.\n"
          },
          "author_id": {
            "type": "integer",
            "description": "Convenience alias for `author.id` (kept for backwards-compat)."
          },
          "author": {
            "$ref": "#/components/schemas/NewsFeedUserRef"
          },
          "requires_acknowledgement": {
            "type": "boolean",
            "description": "Whether THIS POST asks the reader to acknowledge it (must-read).\n\nNote for broadcast-sourced entries (`source.type == \"Broadcast\"`):\nthis is `false` by design even when the broadcast requires an\nacknowledgement — the post is contributed at priority\n`operational`, so the acknowledgement stays on the broadcast's\nledger rather than being minted twice. Read\n`source.requires_acknowledgement` / `source.acknowledged` for those\ncards, and acknowledge via\n`POST /api/v1/broadcasts/{source.id}/acknowledge`.\n"
          },
          "policy": {
            "type": "object",
            "nullable": true,
            "description": "The HR policy a Must-Read asks the reader to accept, plus THIS\ncaller's acceptance of it. `null` when the post names no policy —\nwhich is most posts. Set at compose time via `feed[policy_id]` on\n`POST /feeds`; the selectable policies come from\n`GET /news-feed/policies`, and one policy's detail from\n`GET /news-feed/policies/{id}`.\n\nAcceptance is tracked separately from acknowledging the post: a\nreader can have acknowledged the must-read without having accepted\nthe policy, so render the two states independently.\n",
            "properties": {
              "id": {
                "type": "integer"
              },
              "title": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "enum": [
                  "draft",
                  "published",
                  "archived"
                ],
                "description": "The policy's own lifecycle state."
              },
              "requires_acknowledgment": {
                "type": "boolean",
                "description": "Whether the policy asks to be accepted at all. When false the\ncard should name the policy without promising an accept action.\n(Spelled `acknowledgment`, matching the Policy Hub column.)\n"
              },
              "accepted": {
                "type": "boolean",
                "description": "Whether this caller has accepted the CURRENT version. A policy\nHR has flagged for re-acknowledgment reads `false` here even\nthough an older acceptance row exists — the reader owes a fresh\nacceptance.\n"
              },
              "accepted_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When they accepted. `null` unless `accepted` is true."
              },
              "url": {
                "type": "string",
                "nullable": true,
                "description": "Absolute URL of the mobile Policy Hub screen — the one surface\nthat both reads and accepts, i.e. the target of the \"Read and\naccept\" action. `null` when the policy is no longer published,\nor Policy Hub isn't reachable by THIS caller, so a client never\nrenders a link that only bounces.\n"
              },
              "acknowledge_url": {
                "type": "string",
                "nullable": true,
                "description": "Absolute URL to POST (empty body) to accept the policy in place\n— `POST /api/v1/policy_hub/policies/{id}/acknowledge` — so a\ncard can offer the accept action itself instead of only bouncing\nthe reader out to `url`. Idempotent; on success the endpoint\nreturns the acknowledgment record.\n\n`null` whenever that POST would be refused for a reason this\npayload already knows: the policy is no longer published or\nPolicy Hub isn't reachable by THIS caller, the policy doesn't\nask to be accepted (`requires_acknowledgment: false`), it\nrequires an e-signature on an e-signature-enabled tenant (where\nsigning happens on the web app via `url`), or the policy is not\nthis caller's to accept — no acknowledgment row assigning it and\nthe caller is outside the policy's live audience, which the\nendpoint refuses with 403. That last case is why `url` can be\nnon-null while this is `null`: the Policy Hub screen is readable\nby anyone, and it hides its own accept CTA on the same\ncondition, so a card must not offer an accept action there.\n\nStays non-null once `accepted` is true — the endpoint is\nidempotent, and a reader HR has flagged for re-acknowledgment\nreads `accepted: false` and needs it again. Also stays non-null\nwhen an acknowledgment row exists but the policy's audience was\nnarrowed afterwards: the row IS the assignment and the endpoint\nhonors it, or the reader would be stranded with a pending item\nthey can never clear.\n\nOne refusal is deliberately NOT reflected here: incomplete\nrequired training → 422 `training_required`. That is \"blocked\npending your action\", not \"not yours\" — the web renders a\ndisabled \"Complete Training First\" CTA rather than hiding it —\nso POST and surface the endpoint's message, which names the\ncourses.\n"
              },
              "acknowledge_url_reason": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "not_published",
                  "app_unavailable",
                  "not_applicable",
                  "esignature_required",
                  "not_targeted"
                ],
                "description": "Why there is no in-place accept action — non-null EXACTLY when\n`acknowledge_url` is `null`, and `null` whenever `acknowledge_url`\nis present. A machine-readable code drawn from the acknowledge\nendpoint's own refusal vocabulary, so a client can branch on the\nsame reason whether it reads it here or POSTs and reads the error:\n  * `not_published` — the policy is no longer published (the\n    endpoint answers 404).\n  * `app_unavailable` — the policy is published but Policy Hub is\n    not reachable by THIS caller (403 `app_forbidden`).\n  * `not_applicable` — the policy does not ask to be accepted\n    (`requires_acknowledgment: false`; 422 `not_applicable`).\n  * `esignature_required` — an e-signature policy on an\n    e-signature-enabled tenant; sign from the web app via `url`\n    (422 `esignature_required`).\n  * `not_targeted` — the policy is not this caller's to accept:\n    no acknowledgment row assigns it and the caller is outside\n    the policy's live audience (403 `forbidden`). This is the\n    case where `url` can be non-null while `acknowledge_url` is\n    `null`.\nIncomplete required training is NOT surfaced here — it leaves\n`acknowledge_url` non-null on purpose (see that field).\n"
              }
            },
            "required": [
              "id",
              "title",
              "status",
              "requires_acknowledgment",
              "accepted",
              "accepted_at",
              "url",
              "acknowledge_url",
              "acknowledge_url_reason"
            ],
            "example": {
              "id": 12,
              "title": "Heat Safety Procedure v4",
              "status": "published",
              "requires_acknowledgment": true,
              "accepted": false,
              "accepted_at": null,
              "url": "https://acme.workforce.mangoapps.com/m/apps/policy-hub/policies/12",
              "acknowledge_url": "https://acme.workforce.mangoapps.com/api/v1/policy_hub/policies/12/acknowledge",
              "acknowledge_url_reason": null
            }
          },
          "topics": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "name": {
                  "type": "string"
                }
              }
            }
          },
          "discussion_open": {
            "type": "boolean"
          },
          "discussion_close": {
            "nullable": true,
            "description": "Moderation close metadata. `null` while the discussion is open\n(`discussion_open: true`); otherwise the active DiscussionClose,\nso clients can render the \"Discussion was closed by … on …\"\nbanner. `closed_by` is `null` for an auto-close (the scheduled\njob has no actor).\n",
            "type": "object",
            "properties": {
              "closed_at": {
                "type": "string",
                "format": "date-time"
              },
              "closed_by": {
                "nullable": true,
                "$ref": "#/components/schemas/NewsFeedUserRef"
              }
            }
          },
          "muted": {
            "type": "boolean",
            "description": "True when the caller has muted notifications for this feed\n(NewsFeed::PostNotificationMute row exists for the current user).\n"
          },
          "reactions": {
            "$ref": "#/components/schemas/NewsFeedReactionsAggregate"
          },
          "comments_count": {
            "type": "integer",
            "minimum": 0,
            "description": "Total active comments (top-level + replies) on this feed."
          },
          "last_comment": {
            "nullable": true,
            "description": "Compact preview of the feed's most recent active comment, for the\nlist card. `null` when the feed has no comments. The full thread\n(replies, reactions, attachments) loads on demand from\n`GET /feeds/{id}/comments`.\n",
            "$ref": "#/components/schemas/NewsFeedLastComment"
          },
          "read_count": {
            "type": "integer",
            "minimum": 0,
            "description": "Total distinct readers (ReadRecord rows) on this feed."
          },
          "acknowledged_count": {
            "type": "integer",
            "minimum": 0,
            "description": "Total acknowledgements (AcknowledgementRecord rows). Meaningful\non must-read feeds; always 0 on non-must-read.\n"
          },
          "has_correct_answer": {
            "type": "boolean",
            "description": "Question posts only"
          },
          "correct_answer": {
            "nullable": true,
            "$ref": "#/components/schemas/NewsFeedCorrectAnswer"
          },
          "poll": {
            "nullable": true,
            "description": "Present on Poll feeds (content_type=poll). Option labels (`options[].id/text/position`) are returned on both list and detail; per-option vote counts (`options[].votes`) are detail-only.",
            "$ref": "#/components/schemas/NewsFeedPoll"
          },
          "viewer": {
            "type": "object",
            "description": "Caller-specific read/acknowledgement state, returned on every\nlist row so mobile cards can render the unread indicator and\nmust-read acknowledgement badge without a second roundtrip.\n",
            "properties": {
              "read": {
                "type": "boolean",
                "description": "Whether this feed is in the caller's READ bucket. This is the\nexact negation of the predicate behind the unread badge\n(`GET /api/v1/apps` → communications `unread_count`), this\nendpoint's `meta.unread_counts`, `filter=unread` and the\nunread-first ordering — so a card rendered from `read` can\nnever disagree with the badge.\n\n`false` in two cases: the caller has never opened the feed\n(`read_at` is null), or somebody else has commented since the\ncaller last opened the discussion (`unread_comment_count > 0`,\n`read_at` non-null). Use `read_at.present?` — not `read` — for\n\"has this person ever opened it\", and note a feed can return\n`read: false` with a non-null `read_at`.\n\nA feed drops back to `read: true` when the caller opens the\ndiscussion (`GET /api/v1/feeds/{feed_id}/comments`, which\nadvances the comment-read watermark) or via\n`POST /api/v1/feeds/mark_all_read`. Marking the card seen\n(`POST /api/v1/feeds/{id}/mark_seen`) records the impression\nbut deliberately does NOT clear comment-driven unread.\n"
              },
              "read_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "First-view read receipt — when the caller first opened this\nfeed, or null if they never have. Unaffected by later comment\nactivity (see `read`).\n"
              },
              "last_seen_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "Most recent view of the card (bumped by\n`POST /api/v1/feeds/{id}/mark_seen` on every impression).\nImpression telemetry only — it is NOT the watermark that\ndrives `read` / `unread_comment_count`.\n"
              },
              "acknowledged": {
                "type": "boolean"
              },
              "acknowledged_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "unread_comment_count": {
                "type": "integer",
                "minimum": 0,
                "description": "Number of comments added to this feed since the caller last\nopened its DISCUSSION (created after `comments_read_at`,\nfalling back to `read_at`; excluding the caller's own\ncomments). `0` when the caller has never opened the feed —\nmirrors the web \"N new replies\" card, which only appears\nafter the first view.\n\nDeliberately NOT keyed off `last_seen_at`: scrolling a card\npast the viewport must not silently clear replies the caller\nhas not read. This is the same watermark `read` and the\nunread badge use, so the three cannot drift.\n"
              }
            }
          },
          "media": {
            "type": "array",
            "description": "Feed-post attachments (image / gif / video / file / link_preview).\nReturned on both list rows and the detail endpoint so cards can\nrender thumbnails / play buttons without a second roundtrip.\nEmpty array when the post has no attachments. Same shape as\ncomment attachments — see NewsFeedFeedMedia.\n",
            "items": {
              "$ref": "#/components/schemas/NewsFeedFeedMedia"
            }
          }
        },
        "required": [
          "id",
          "content_type",
          "content_category",
          "priority",
          "status",
          "audience_type",
          "author_id",
          "author",
          "reactions",
          "comments_count",
          "read_count",
          "acknowledged_count",
          "muted",
          "viewer",
          "media"
        ]
      },
      "NewsFeedLastComment": {
        "type": "object",
        "nullable": true,
        "description": "Compact preview of a feed's most recent active comment, embedded on\nlist rows (`NewsFeedSummary.last_comment`). Intentionally minimal —\nthe full comment object (replies, reactions, attachments,\nis_correct_answer) is returned by `GET /feeds/{id}/comments`.\n",
        "properties": {
          "id": {
            "type": "integer"
          },
          "author": {
            "$ref": "#/components/schemas/NewsFeedUserRef"
          },
          "body": {
            "type": "string",
            "description": "Comment text. Empty string for a soft-deleted comment."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "author",
          "body",
          "created_at"
        ]
      },
      "NewsFeedUserRef": {
        "type": "object",
        "nullable": true,
        "description": "Author / user reference shape used across the News Feed API. Mirrors the\n`author` block on `NewsFeedComment` so mobile clients have a single\nrenderer. `avatar_url` is the resolved CDN URL (helpers.avatar_url) and\nmay point to a default avatar when the user hasn't uploaded one.\n",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "avatar_url": {
            "type": "string",
            "nullable": true,
            "format": "uri"
          }
        }
      },
      "NewsFeedReactionsAggregate": {
        "type": "object",
        "description": "Aggregate reactions (from `NewsFeed::ReactionsService.aggregate`).\n`my_reaction` is the caller's own emoji_key or null.\n",
        "properties": {
          "total": {
            "type": "integer",
            "minimum": 0
          },
          "top_emojis": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "emoji_key": {
                  "type": "string"
                },
                "count": {
                  "type": "integer",
                  "minimum": 0
                }
              }
            }
          },
          "my_reaction": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "NewsFeedDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/NewsFeedSummary"
          },
          {
            "type": "object",
            "properties": {
              "rendered_body": {
                "type": "string",
                "description": "Server-rendered Markdown HTML for the body"
              },
              "poll": {
                "nullable": true,
                "$ref": "#/components/schemas/NewsFeedPoll"
              },
              "ai_summary": {
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/NewsFeedDiscussionSummary"
                  }
                ],
                "description": "PRD 14 §11 — Embedded AI discussion summary. Mirrors the\n`_discussion_summary_card.html.erb` web partial: returned\nwhen the tenant has `ai_discussion_summarization` enabled\nand a row exists for the feed, else `null`. Both\n`succeeded` and `failed` rows are surfaced so clients can\nrender the same \"couldn't be generated\" fallback the web\ncard shows. Detail-only — list rows omit this field.\n"
              },
              "poll_summary": {
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/NewsFeedPollSummary"
                  }
                ],
                "description": "PRD 15 §11 — Embedded AI poll outcome summary. Mirrors the\n`_poll_summary_card.html.erb` web partial: returned only\nwhen the tenant has `ai_poll_summarization` enabled, the\npoll is closed, and the latest summary `succeeded`. Failed\nrows are admin-visible only and surface as `null` here.\nDetail-only and present only on `content_type=poll` feeds.\n"
              }
            }
          }
        ]
      },
      "NewsFeedDiscussionSummary": {
        "type": "object",
        "description": "PRD 14 §11 — JSON view of `NewsFeed::DiscussionSummary`. Returned\nembedded on `NewsFeedDetail.ai_summary` and standalone from\n`GET /feeds/{id}/summary`.\n",
        "properties": {
          "id": {
            "type": "integer"
          },
          "feed_id": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "succeeded",
              "failed"
            ]
          },
          "trigger": {
            "type": "string",
            "enum": [
              "threshold",
              "manual_close",
              "author_request"
            ]
          },
          "comment_count_at_generation": {
            "type": "integer",
            "minimum": 0
          },
          "generated_at": {
            "type": "string",
            "format": "date-time"
          },
          "model_version": {
            "type": "string",
            "nullable": true
          },
          "failure_reason": {
            "type": "string",
            "nullable": true,
            "description": "Populated when status == failed."
          },
          "summary": {
            "type": "object",
            "description": "Structured payload extracted from `summary_payload`. Always\npresent (even when `status == failed`, in which case fields\nbelow may be blank / default values). `decisions` defaults to\n`\"None identified\"` when the underlying payload field is\nblank — mirrored from `NewsFeed::DiscussionSummary#decisions`\nso the web view and the API agree without duplicating copy.\n",
            "properties": {
              "takeaways": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "decisions": {
                "type": "string"
              },
              "action_items": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string",
                      "nullable": true
                    },
                    "action": {
                      "type": "string"
                    },
                    "deadline": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              },
              "sentiment": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "positive",
                  "neutral",
                  "negative",
                  "mixed"
                ]
              },
              "sentiment_reason": {
                "type": "string",
                "nullable": true
              },
              "question_answer_status": {
                "type": "string",
                "nullable": true
              },
              "correct_answer_excerpt": {
                "type": "string",
                "nullable": true
              }
            }
          }
        }
      },
      "NewsFeedPollSummary": {
        "type": "object",
        "description": "PRD 15 §11 — JSON view of `NewsFeed::PollSummary`. Returned\nembedded on `NewsFeedDetail.poll_summary` and standalone from\n`GET /feeds/{id}/poll_summary`.\n",
        "properties": {
          "id": {
            "type": "integer"
          },
          "feed_id": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "succeeded",
              "failed"
            ]
          },
          "trigger": {
            "type": "string",
            "enum": [
              "poll_close",
              "admin_replay"
            ]
          },
          "generated_at": {
            "type": "string",
            "format": "date-time"
          },
          "model_version": {
            "type": "string",
            "nullable": true
          },
          "failure_reason": {
            "type": "string",
            "nullable": true
          },
          "summary_text": {
            "type": "string",
            "nullable": true,
            "maxLength": 500,
            "description": "Human-readable 2-3 sentence outcome summary (PRD §10 — max 500 chars). Null when status == failed."
          },
          "summary": {
            "type": "object",
            "properties": {
              "leading_option_id": {
                "type": "integer",
                "nullable": true
              },
              "leading_option_text": {
                "type": "string",
                "nullable": true
              },
              "winning_percentage": {
                "type": "number",
                "nullable": true
              },
              "participation_rate": {
                "type": "number",
                "description": "Fraction in [0, 1]."
              },
              "low_participation": {
                "type": "boolean"
              },
              "is_anonymous": {
                "type": "boolean"
              },
              "segment_patterns": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          }
        }
      },
      "NewsFeedCorrectAnswer": {
        "type": "object",
        "description": "Verified-answer block returned on Question posts that have a marked\ncorrect answer. Returned both embedded on `NewsFeedSummary.correct_answer`\nand standalone from `GET /feeds/{id}/correct_answer`. `marked_by_user_id`\nidentifies the admin / author who **marked** the answer as verified;\n`answer.author` is the user who actually **wrote** the answer comment.\n",
        "properties": {
          "id": {
            "type": "integer"
          },
          "feed_id": {
            "type": "integer"
          },
          "comment_id": {
            "type": "integer"
          },
          "marked_at": {
            "type": "string",
            "format": "date-time"
          },
          "marked_by_user_id": {
            "type": "integer"
          },
          "answer": {
            "nullable": true,
            "type": "object",
            "description": "The marked answer comment + the user who wrote it. Mirrors the\nweb \"Correct Answer\" pinned card. `null` when the comment has\nbeen soft-deleted or hard-deleted while the outer\n`correct_answer` record persists — mirrors the web partial\nwhich renders a \"[the marked comment is no longer available]\"\nplaceholder in that case. The outer `comment_id` /\n`marked_at` / `marked_by_user_id` are still returned for audit.\n",
            "properties": {
              "id": {
                "type": "integer",
                "description": "ID of the marked comment."
              },
              "body": {
                "type": "string",
                "description": "Comment body as authored (plain text per PRD §13)."
              },
              "status": {
                "type": "string",
                "enum": [
                  "active",
                  "deleted"
                ]
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "edited_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "author": {
                "$ref": "#/components/schemas/NewsFeedUserRef",
                "description": "User who wrote the marked answer. Distinct from\n`marked_by_user_id`, which is the user who marked it as\nverified (typically the post author or an admin).\n"
              }
            }
          }
        }
      },
      "NewsFeedPoll": {
        "type": "object",
        "description": "Poll state embedded on Poll-type feeds. Returned on both list rows\nand the detail endpoint. Option labels (`options[].id/text/position`)\nare included on both so cards can render the option list from the\nlist response; per-option vote counts (`options[].votes`) are\ndetail-only to keep list payloads light.\n",
        "properties": {
          "closes_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "voting_mode": {
            "type": "string",
            "nullable": true,
            "enum": [
              "single",
              "multi",
              "ranked"
            ],
            "description": "Controls how `my_vote_option_ids` should be interpreted:\n  * `single` — at most 1 element\n  * `multi`  — 0..N elements, order is not meaningful\n  * `ranked` — 0..N elements, ordered by rank (most-preferred first)\n"
          },
          "is_anonymous": {
            "type": "boolean",
            "nullable": true
          },
          "result_visibility": {
            "type": "string",
            "nullable": true,
            "enum": [
              "live",
              "hidden_until_close"
            ]
          },
          "allow_change_vote": {
            "type": "boolean",
            "nullable": true
          },
          "allow_comments": {
            "type": "boolean",
            "description": "Whether a comment may be posted on the poll right now\n(`Feed#comments_allowed?`). Folds the author's create-time\ncomment setting (the feed-level `comments_enabled` column) together\nwith the moderation discussion-close state, so poll cards gate their\ncomment input from one field. Set it at create/update time via\n`poll_config_attributes[allow_comments]` (or top-level\n`feed[allow_comments]`).\n"
          },
          "is_closed": {
            "type": "boolean",
            "description": "True when poll_config.closes_at has elapsed."
          },
          "option_count": {
            "type": "integer",
            "minimum": 0,
            "description": "Number of poll options."
          },
          "my_vote_option_ids": {
            "type": "array",
            "description": "The caller's voted option_ids. Always present; empty array when\nthe caller has not voted. Interpret via `voting_mode`:\n  * `single` — at most 1 element\n  * `multi`  — 0..N elements, order not meaningful\n  * `ranked` — 0..N elements, ordered by rank (most-preferred first)\n",
            "items": {
              "type": "integer"
            }
          },
          "results_visible": {
            "type": "boolean",
            "description": "Detail-only. True when the caller is allowed to see the\nper-option vote breakdown; false when results are gated.\n\nGating rules (PRD 05 FR-05-06 / FR-05-10), shared with the web\nsurface:\n  * Closed poll → true (everyone)\n  * Author or business admin → true (always)\n  * Open + `result_visibility: hidden_until_close` → false\n  * Open + `result_visibility: live`, caller has voted → true\n  * Open + `result_visibility: live`, caller has not voted → false\n\nWhen false, `total_votes` and each `options[].votes` /\n`options[].percent` are returned as `null` so clients can render\na \"results hidden\" state without inferring whether anyone has\nvoted yet.\n"
          },
          "total_votes": {
            "type": "integer",
            "minimum": 0,
            "nullable": true,
            "description": "Aggregate count of distinct voters (sum of active PollVote rows).\nDetail-only — returned on `GET /feeds/{id}` and on the\n`/feeds/{id}/poll_votes` responses, omitted from list rows.\n`null` when `results_visible` is false.\n"
          },
          "options": {
            "type": "array",
            "description": "Poll options. `id`, `text`, and `position` are returned on both\nlist rows and the detail endpoint. `votes` (per-option count)\nand `percent` (0–100, rounded) are detail-only — list rows omit\nthem. Both are `null` when `results_visible` is false.\n",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "text": {
                  "type": "string"
                },
                "position": {
                  "type": "integer"
                },
                "votes": {
                  "type": "integer",
                  "nullable": true,
                  "description": "Detail endpoint only. `null` when results are gated."
                },
                "percent": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 100,
                  "nullable": true,
                  "description": "Detail endpoint only. Share of `total_votes` cast for this\noption, rounded to the nearest integer (0–100). `null` when\nresults are gated. Matches the value rendered by the web\n`_poll` partial so mobile and web stay in lockstep.\n"
                }
              }
            }
          }
        }
      },
      "NewsFeedComment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "feed_id": {
            "type": "integer"
          },
          "parent_comment_id": {
            "type": "integer",
            "nullable": true
          },
          "depth": {
            "type": "integer",
            "minimum": 0,
            "maximum": 1
          },
          "author": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "avatar_url": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "body": {
            "type": "string",
            "description": "Comment text. Empty string for soft-deleted comments and for\nattachment-only comments (the server stores a zero-width-space\nsentinel which the serializer surfaces verbatim — clients should\ntreat `body.trim()` as the display value).\n"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "deleted"
            ]
          },
          "edited_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "topics": {
            "type": "array",
            "description": "Topics assigned to this comment — author hashtags (`#tag` in the\nbody) plus AI-classified topics. At most 3, ordered by assignment\ntime. Same shape as a feed post's `topics`. A comment carrying a\ntopic also makes its parent feed match that topic in\n`GET /feeds?topic_id=`.\n",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "name": {
                  "type": "string"
                }
              }
            }
          },
          "replies_count": {
            "type": "integer"
          },
          "replies": {
            "type": "array",
            "description": "Direct child comments (depth-1 replies) inlined on top-level\ncomments. Each reply is a full `NewsFeedComment` payload —\nsame shape, including its own `media[]` for attachments.\nAlways empty (`[]`) on a reply, since `MAX_DEPTH = 1`.\nOrdered by `created_at` ascending.\n",
            "items": {
              "$ref": "#/components/schemas/NewsFeedComment"
            }
          },
          "reactions": {
            "$ref": "#/components/schemas/NewsFeedReactionsAggregate"
          },
          "media": {
            "type": "array",
            "description": "Attachments on this comment (PRD 04 Phase 16). Empty when the\ncomment has no media. Same shape as feed-post attachments.\n",
            "items": {
              "$ref": "#/components/schemas/NewsFeedFeedMedia"
            }
          },
          "is_correct_answer": {
            "type": "boolean"
          }
        }
      },
      "NewsFeedFeedMedia": {
        "type": "object",
        "description": "FeedMediaSerializer payload — used for both feed-post and comment\nattachments. `feed_id` is set once the row is claimed onto a post;\n`comment_id` is set once the row is claimed onto a comment. While\nthe row is still orphan (uploaded but not yet attached), both are\nnull.\n",
        "properties": {
          "id": {
            "type": "integer"
          },
          "feed_id": {
            "type": "integer",
            "nullable": true
          },
          "comment_id": {
            "type": "integer",
            "nullable": true
          },
          "media_type": {
            "type": "string",
            "enum": [
              "image",
              "gif",
              "video",
              "file",
              "link_preview"
            ]
          },
          "processing_status": {
            "type": "string",
            "enum": [
              "pending",
              "ready",
              "failed"
            ]
          },
          "original_filename": {
            "type": "string",
            "nullable": true
          },
          "mime_type": {
            "type": "string",
            "nullable": true
          },
          "file_size_bytes": {
            "type": "integer",
            "nullable": true
          },
          "storage_url": {
            "type": "string",
            "nullable": true,
            "description": "Presigned S3 URL or CDN URL — short-lived."
          },
          "hls_playlist_url": {
            "type": "string",
            "nullable": true,
            "description": "Set for transcoded videos."
          },
          "thumbnail_url": {
            "type": "string",
            "nullable": true
          },
          "alt_text": {
            "type": "string",
            "nullable": true
          },
          "width_px": {
            "type": "integer",
            "nullable": true
          },
          "height_px": {
            "type": "integer",
            "nullable": true
          },
          "position": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SafetyHubPaginationMeta": {
        "type": "object",
        "description": "Pagination metadata as returned by Api::V1::SafetyHubController#pagination_meta.\nField shape differs from the global PaginationMeta (this one uses `page`/`total`,\nthe global uses `current_page`/`total_count`).\n",
        "properties": {
          "page": {
            "type": "integer",
            "example": 1
          },
          "per_page": {
            "type": "integer",
            "example": 25
          },
          "total": {
            "type": "integer",
            "example": 137
          },
          "total_pages": {
            "type": "integer",
            "example": 6
          }
        },
        "required": [
          "page",
          "per_page",
          "total",
          "total_pages"
        ]
      },
      "SafetyHubPersonRef": {
        "type": "object",
        "nullable": true,
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "SafetyHubLocationRef": {
        "type": "object",
        "nullable": true,
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "Incident": {
        "type": "object",
        "description": "Safety Hub incident (list serializer)",
        "properties": {
          "id": {
            "type": "integer",
            "example": 117
          },
          "reference": {
            "type": "string",
            "example": "INC-117",
            "description": "Human-facing reference (\"INC-<id>\")"
          },
          "title": {
            "type": "string",
            "example": "Slip on wet floor"
          },
          "incident_type": {
            "type": "string",
            "enum": [
              "injury",
              "near_miss",
              "property_damage",
              "environmental",
              "security",
              "vehicle_accident",
              "equipment_failure"
            ]
          },
          "severity": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high",
              "critical"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "reported",
              "investigating",
              "investigation_completed",
              "closed",
              "cancelled"
            ]
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time"
          },
          "location": {
            "$ref": "#/components/schemas/SafetyHubLocationRef"
          },
          "reporter": {
            "$ref": "#/components/schemas/SafetyHubPersonRef"
          },
          "anonymous": {
            "type": "boolean"
          },
          "osha_recordable": {
            "type": "boolean"
          }
        }
      },
      "IncidentDetail": {
        "description": "Safety Hub incident (detail serializer — adds investigation fields)",
        "allOf": [
          {
            "$ref": "#/components/schemas/Incident"
          },
          {
            "type": "object",
            "properties": {
              "description": {
                "type": "string",
                "nullable": true
              },
              "immediate_actions": {
                "type": "string",
                "nullable": true
              },
              "location_specific_notes": {
                "type": "string",
                "nullable": true,
                "description": "Sub-location / area within the location"
              },
              "osha_status": {
                "type": "string",
                "enum": [
                  "recordable",
                  "not_recordable",
                  "under_review",
                  "not_applicable"
                ],
                "description": "Recordability label; \"under_review\" until a determination is stamped, \"not_applicable\" for a non-injury incident (never an OSHA candidate)"
              },
              "investigator": {
                "$ref": "#/components/schemas/SafetyHubPersonRef"
              },
              "witnesses": {
                "type": "array",
                "description": "Witness roster. PRIVILEGED — empty for a plain reporter; only a site manager or the assigned investigator sees names (matches the desktop Witnesses card).",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "name": {
                      "type": "string"
                    }
                  }
                }
              },
              "investigation_due_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "investigation_started_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "investigation_completed_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "investigation_overdue": {
                "type": "boolean",
                "description": "True when the investigation deadline has passed and the investigation is neither completed nor the incident closed/cancelled."
              },
              "media": {
                "type": "array",
                "description": "Incident scene photos & videos (\"Photos & Videos\" section), in display order. Visible to any viewer who can reach the record.",
                "items": {
                  "$ref": "#/components/schemas/SafetyHubMediaItem"
                }
              },
              "investigation": {
                "nullable": true,
                "description": "Latest investigation narrative. Null unless the caller is a PII viewer (site manager or assigned investigator).",
                "type": "object",
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "investigation_type": {
                    "type": "string",
                    "nullable": true
                  },
                  "status": {
                    "type": "string"
                  },
                  "summary": {
                    "type": "string",
                    "nullable": true
                  },
                  "root_cause": {
                    "type": "string",
                    "nullable": true
                  },
                  "findings": {
                    "type": "string",
                    "nullable": true
                  },
                  "recommendations": {
                    "type": "string",
                    "nullable": true
                  },
                  "started_at": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true
                  },
                  "completed_at": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true
                  }
                }
              },
              "corrective_actions": {
                "type": "array",
                "description": "CAPA register for this incident (see SafetyHubCorrectiveAction schema)",
                "items": {
                  "$ref": "#/components/schemas/SafetyHubCorrectiveAction"
                }
              },
              "activity_count": {
                "type": "integer",
                "description": "Number of activity-timeline entries (incident updates)."
              },
              "activity": {
                "type": "array",
                "description": "Activity timeline (incident updates), newest first — the detail\nscreen's \"Activity\" card. NOT privileged: visible to any viewer\nwho can reach the record, matching the desktop show page.\n",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "type": {
                      "type": "string",
                      "description": "Raw update_type (status_change, investigation_note, evidence_added, witness_statement, corrective_action, follow_up, closure)."
                    },
                    "type_label": {
                      "type": "string",
                      "description": "Human label for the update type."
                    },
                    "content": {
                      "type": "string"
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "user": {
                      "nullable": true,
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string"
                        },
                        "initials": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              },
              "compliance": {
                "nullable": true,
                "description": "OSHA/WCB determination record backing the \"OSHA\" detail row and\nthe manager \"Determine OSHA / WCB Reportability\" workflow.\nPRIVILEGED — null unless the caller is a PII viewer (site manager\nor assigned investigator), matching the desktop OSHA + WCB cards.\nThe `wcb` sub-block is present only when the tenant has WCB\ncompliance enabled.\n",
                "type": "object",
                "properties": {
                  "is_injury": {
                    "type": "boolean"
                  },
                  "osha_recordable": {
                    "type": "boolean",
                    "nullable": true
                  },
                  "osha_classification": {
                    "type": "string",
                    "nullable": true,
                    "description": "e.g. \"Fatality\", \"Days Away From Work\", \"Job Transfer or Restriction\", \"Other Recordable Cases\". Null until recordable."
                  },
                  "fatality": {
                    "type": "boolean"
                  },
                  "days_away_from_work": {
                    "type": "integer"
                  },
                  "days_restricted_work": {
                    "type": "integer"
                  },
                  "medical_attention_people": {
                    "type": "array",
                    "description": "Involved people flagged as needing medical attention.",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "wcb": {
                    "type": "object",
                    "description": "Present only when WCB compliance is enabled for the tenant.",
                    "properties": {
                      "reportable": {
                        "type": "boolean",
                        "nullable": true
                      },
                      "classification": {
                        "type": "string",
                        "nullable": true
                      },
                      "province": {
                        "type": "string",
                        "nullable": true
                      },
                      "reporting_deadline": {
                        "type": "string",
                        "format": "date",
                        "nullable": true
                      },
                      "claim_number": {
                        "type": "string",
                        "nullable": true
                      }
                    }
                  }
                }
              },
              "closed_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        ]
      },
      "SafetyHubIncidentPermissions": {
        "type": "object",
        "description": "Viewer-relative capability flags for the incident detail screen, so a\nclient renders the right chrome without a second round trip. The\ndetail screen's only persona split is the manager-only \"Investigation\nWorkflow\" block.\n",
        "properties": {
          "can_view_investigation": {
            "type": "boolean",
            "description": "Viewer may see the investigation narrative (findings/root cause/recommendations) — a site manager or the assigned investigator."
          },
          "can_manage_investigation": {
            "type": "boolean",
            "description": "Viewer may run the Investigation Workflow (reassign/complete/close/determine OSHA-WCB) — a site manager for the incident."
          },
          "can_edit": {
            "type": "boolean",
            "description": "Viewer may edit the incident (reporter or site manager, and the incident is in an editable state)."
          },
          "can_close": {
            "type": "boolean",
            "description": "Viewer may close the incident now (a site manager, and nothing blocks closure). False for non-managers and whenever close_blocker is set."
          },
          "close_blocker": {
            "type": "string",
            "nullable": true,
            "description": "User-facing reason the incident cannot be closed yet (the mockup's\n\"Close Incident\" blocker line), or null when it can. Computed only\nfor a site manager; always null for other viewers.\n"
          },
          "is_reporter": {
            "type": "boolean"
          },
          "is_investigator": {
            "type": "boolean"
          }
        }
      },
      "SafetyHubIncidentCreateRequest": {
        "type": "object",
        "description": "Body for reporting a new incident (POST /safety_hub/incidents). Wrapped\nunder an `incident` key. Optional people/witness rows sit alongside it at\nthe top level, mirroring the web form. Photos are sent as multipart\n`photos[]` (see the endpoint's multipart schema), never inside this\nobject.\n",
        "required": [
          "incident"
        ],
        "properties": {
          "incident": {
            "type": "object",
            "required": [
              "title",
              "description",
              "incident_type",
              "severity",
              "occurred_at"
            ],
            "properties": {
              "title": {
                "type": "string",
                "minLength": 3,
                "maxLength": 255,
                "example": "Forklift clipped a rack leg"
              },
              "description": {
                "type": "string",
                "minLength": 10,
                "maxLength": 5000
              },
              "incident_type": {
                "type": "string",
                "enum": [
                  "injury",
                  "near_miss",
                  "property_damage",
                  "environmental",
                  "security",
                  "vehicle_accident",
                  "equipment_failure",
                  "other"
                ]
              },
              "severity": {
                "type": "string",
                "enum": [
                  "low",
                  "medium",
                  "high",
                  "critical"
                ]
              },
              "occurred_at": {
                "type": "string",
                "format": "date-time",
                "description": "When the incident occurred (must not be in the future)."
              },
              "location_id": {
                "type": "integer",
                "nullable": true,
                "description": "A site in the caller's business; a foreign id is dropped rather than rejected."
              },
              "immediate_actions": {
                "type": "string",
                "nullable": true
              },
              "anonymous": {
                "type": "boolean",
                "default": false,
                "description": "When true, the reporter identity is withheld from the response and every read surface."
              },
              "confidential": {
                "type": "boolean",
                "default": false
              },
              "days_away_from_work": {
                "type": "integer",
                "nullable": true,
                "minimum": 0,
                "maximum": 9999
              },
              "days_restricted_work": {
                "type": "integer",
                "nullable": true,
                "minimum": 0,
                "maximum": 9999
              },
              "fatality": {
                "type": "boolean",
                "default": false
              },
              "wcb_province": {
                "type": "string",
                "nullable": true
              },
              "alert_id": {
                "type": "integer",
                "nullable": true,
                "description": "Optional emergency Alert this report opened from; a foreign id is dropped."
              }
            }
          },
          "people": {
            "type": "array",
            "description": "Optional people-involved rows. A row that fails validation is returned as a warning, not an error.",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "person_type": {
                  "type": "string",
                  "enum": [
                    "employee",
                    "contractor",
                    "visitor",
                    "customer"
                  ]
                },
                "role": {
                  "type": "string",
                  "nullable": true
                },
                "contact_info": {
                  "type": "string",
                  "nullable": true
                },
                "injury_type": {
                  "type": "string",
                  "enum": [
                    "none",
                    "first_aid",
                    "medical_treatment",
                    "hospitalization"
                  ],
                  "nullable": true
                },
                "body_parts_affected": {
                  "type": "string",
                  "nullable": true
                },
                "medical_attention": {
                  "type": "boolean",
                  "nullable": true
                }
              }
            }
          },
          "witnesses": {
            "type": "array",
            "description": "Optional witness rows. A row that fails validation is returned as a warning, not an error.",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "contact_info": {
                  "type": "string",
                  "nullable": true
                },
                "relationship_to_incident": {
                  "type": "string",
                  "nullable": true
                },
                "statement": {
                  "type": "string",
                  "nullable": true
                }
              }
            }
          }
        }
      },
      "SafetyObservation": {
        "type": "object",
        "description": "Safety observation (list serializer)",
        "properties": {
          "id": {
            "type": "integer",
            "example": 44
          },
          "observation_type": {
            "type": "string",
            "enum": [
              "positive",
              "at_risk",
              "near_miss"
            ]
          },
          "category": {
            "type": "string",
            "example": "PPE"
          },
          "status": {
            "type": "string",
            "enum": [
              "submitted",
              "under_review",
              "resolved",
              "closed"
            ]
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "Observation headline/body (observations have no separate title)"
          },
          "observed_at": {
            "type": "string",
            "format": "date-time"
          },
          "location": {
            "$ref": "#/components/schemas/SafetyHubLocationRef"
          },
          "observer": {
            "$ref": "#/components/schemas/SafetyHubPersonRef"
          },
          "anonymous": {
            "type": "boolean"
          },
          "follow_up_required": {
            "type": "boolean"
          }
        }
      },
      "SafetyObservationDetail": {
        "description": "Safety observation (detail serializer). Adds the follow-up fields, the\nattached photos & videos gallery (`media`), and the drive the\nobservation was filed against (`campaign`). Viewable by the observer who\nsubmitted it, or by a Safety Hub manager whose accessible sites include\nthe observation's site (a site-less observation stays manager-visible).\n",
        "allOf": [
          {
            "$ref": "#/components/schemas/SafetyObservation"
          },
          {
            "type": "object",
            "properties": {
              "specific_location": {
                "type": "string",
                "nullable": true,
                "description": "Sub-location / area within the location"
              },
              "action_taken": {
                "type": "string",
                "nullable": true
              },
              "follow_up_notes": {
                "type": "string",
                "nullable": true
              },
              "follow_up_completed_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "follow_up_completed_by": {
                "$ref": "#/components/schemas/SafetyHubPersonRef"
              },
              "campaign": {
                "$ref": "#/components/schemas/SafetyObservationCampaignRef"
              },
              "media": {
                "type": "array",
                "description": "Attached photos & videos, in display order. Empty when none are attached.",
                "items": {
                  "$ref": "#/components/schemas/SafetyHubMediaItem"
                }
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        ]
      },
      "SafetyObservationCreate": {
        "type": "object",
        "description": "Fields for submitting a safety observation (POST /safety_hub/observations).\nOnly observation_type, category and description are required; observed_at\ndefaults to now when omitted. `status` cannot be set — a new observation\nalways starts `submitted`. A location_id / safety_observation_campaign_id\nthe caller's tenant does not own is dropped rather than rejected.\n",
        "required": [
          "observation_type",
          "category",
          "description"
        ],
        "properties": {
          "observation_type": {
            "type": "string",
            "enum": [
              "positive",
              "at_risk",
              "near_miss"
            ],
            "description": "positive = safe behaviour to reinforce; at_risk / near_miss both notify managers + safety officers."
          },
          "category": {
            "type": "string",
            "example": "Housekeeping",
            "description": "Free-form category from the tenant's configured list."
          },
          "description": {
            "type": "string",
            "maxLength": 5000,
            "description": "What did you observe? (the headline — observations have no separate title)."
          },
          "observed_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the observation was made. Defaults to now when omitted; cannot be in the future."
          },
          "specific_location": {
            "type": "string",
            "maxLength": 255,
            "description": "Sub-location / area within the location (\"Bay 3 — mixing station\")."
          },
          "location_id": {
            "type": "integer",
            "description": "A location owned by the caller's business; a foreign id is dropped."
          },
          "action_taken": {
            "type": "string",
            "description": "Immediate action the observer took (optional)."
          },
          "follow_up_required": {
            "type": "boolean",
            "description": "Keep the observation open until it is closed out."
          },
          "follow_up_notes": {
            "type": "string",
            "description": "Follow-up detail (optional)."
          },
          "safety_observation_campaign_id": {
            "type": "integer",
            "description": "An active observation drive owned by the caller's business; a foreign id is dropped."
          }
        }
      },
      "SafetyHubMediaItem": {
        "type": "object",
        "description": "A photo or video attached to a Safety Hub record. Same shape as the\ninspections media payload; `url` is an absolute, signed download URL\n(1-hour expiry) and blanks to null only for a purged/unattached blob.\n",
        "properties": {
          "id": {
            "type": "integer"
          },
          "media_kind": {
            "type": "string",
            "enum": [
              "photo",
              "video",
              "voice_memo"
            ],
            "example": "photo"
          },
          "content_type": {
            "type": "string",
            "example": "image/jpeg"
          },
          "byte_size": {
            "type": "integer",
            "nullable": true,
            "example": 2097152
          },
          "original_filename": {
            "type": "string",
            "nullable": true,
            "example": "walkway-hazard.jpg"
          },
          "url": {
            "type": "string",
            "nullable": true,
            "example": "https://acme.workforce.mangoapps.com/rails/active_storage/blobs/redirect/abc/walkway-hazard.jpg"
          },
          "thumb_url": {
            "type": "string",
            "nullable": true,
            "example": "https://acme.workforce.mangoapps.com/rails/active_storage/representations/.../walkway-hazard.jpg"
          },
          "captured_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        },
        "required": [
          "id",
          "media_kind"
        ]
      },
      "SafetyObservationCampaignRef": {
        "type": "object",
        "nullable": true,
        "description": "The observation drive this observation was filed against, or null when\nunattributed. Disclosure only — the reward is surfaced so the reporter\nknows the drive offers one; nothing in this API awards it.\n",
        "properties": {
          "id": {
            "type": "integer",
            "example": 12
          },
          "name": {
            "type": "string",
            "example": "Q3 Housekeeping Drive"
          },
          "reward": {
            "type": "object",
            "nullable": true,
            "description": "Present only when the drive enables a reward with a description or a positive point value.",
            "properties": {
              "description": {
                "type": "string",
                "nullable": true,
                "example": "Coffee voucher"
              },
              "points": {
                "type": "integer",
                "nullable": true,
                "example": 50
              }
            }
          }
        }
      },
      "ToolboxTalk": {
        "type": "object",
        "description": "Toolbox talk (list serializer)",
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "scheduled",
              "in_progress",
              "completed",
              "cancelled"
            ]
          },
          "scheduled_at": {
            "type": "string",
            "format": "date-time"
          },
          "duration_minutes": {
            "type": "integer",
            "nullable": true
          },
          "facilitator": {
            "$ref": "#/components/schemas/SafetyHubPersonRef"
          },
          "topic": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "title": {
                "type": "string"
              }
            }
          },
          "location": {
            "$ref": "#/components/schemas/SafetyHubLocationRef"
          },
          "meeting_location": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "ToolboxTalkDetail": {
        "description": "Toolbox talk (detail serializer — adds completion + attendance)",
        "allOf": [
          {
            "$ref": "#/components/schemas/ToolboxTalk"
          },
          {
            "type": "object",
            "properties": {
              "description": {
                "type": "string",
                "nullable": true
              },
              "notes": {
                "type": "string",
                "nullable": true
              },
              "started_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "completed_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "actual_duration_minutes": {
                "type": "integer",
                "nullable": true
              },
              "attendance_count": {
                "type": "integer"
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        ]
      },
      "ToolboxTalkTopic": {
        "type": "object",
        "description": "Toolbox talk topic template",
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "duration_estimate": {
            "type": "integer",
            "nullable": true
          },
          "content_preview": {
            "type": "string",
            "nullable": true
          },
          "system_template": {
            "type": "boolean"
          },
          "times_used": {
            "type": "integer"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SafetyCertificationRequirement": {
        "type": "object",
        "description": "Safety Hub certification REQUIREMENT (template). Distinct from the global\n`Certification` schema, which represents an employee's earned certification.\n",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "mandatory": {
            "type": "boolean"
          },
          "active": {
            "type": "boolean"
          },
          "validity_period_months": {
            "type": "integer",
            "nullable": true
          },
          "renewal_reminder_days": {
            "type": "integer",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SafetyHubCorrectiveAction": {
        "type": "object",
        "description": "One row from GET /safety_hub/corrective_actions — a corrective/preventive\naction (Capa::Action), shaped for the mobile \"My Corrective Actions\" list\nand detail. Priority/status carry the same Bootstrap color tokens the web\nbadges use, so a native client renders the identical treatment.\n",
        "properties": {
          "id": {
            "type": "integer"
          },
          "summary": {
            "type": "string",
            "description": "One-line, markdown-stripped rendering of the description (the web list title)."
          },
          "description": {
            "type": "string",
            "description": "Full action description."
          },
          "action_type": {
            "type": "string",
            "nullable": true,
            "description": "Raw action type (immediate/short_term/long_term/preventive/…), null when unset."
          },
          "action_type_label": {
            "type": "string",
            "description": "Display label for the action type (\"Corrective\" when unset)."
          },
          "priority": {
            "type": "object",
            "properties": {
              "value": {
                "type": "string",
                "nullable": true
              },
              "label": {
                "type": "string",
                "nullable": true
              },
              "color": {
                "type": "string",
                "description": "Bootstrap color token (danger/warning/info/secondary)"
              }
            }
          },
          "status": {
            "type": "object",
            "properties": {
              "value": {
                "type": "string",
                "nullable": true,
                "description": "pending/in_progress/completed/cancelled"
              },
              "label": {
                "type": "string",
                "nullable": true
              },
              "color": {
                "type": "string",
                "description": "Bootstrap color token"
              }
            }
          },
          "source": {
            "type": "object",
            "description": "Where the action came from.",
            "properties": {
              "label": {
                "type": "string",
                "description": "Inspection / Visit Finding / Incident / Other"
              },
              "type": {
                "type": "string",
                "nullable": true,
                "description": "Raw polymorphic source_type, null for a manually-raised action"
              },
              "incident": {
                "type": "object",
                "nullable": true,
                "description": "Present only for incident-sourced rows. `url` is a mobile deep\nlink, but only when the caller may view the incident (a manager,\nor the incident's own reporter); otherwise `null` — the reference\nlabel is still shown.\n",
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "reference": {
                    "type": "string",
                    "example": "INC-123"
                  },
                  "url": {
                    "type": "string",
                    "nullable": true,
                    "example": "/m/apps/safety-hub/incidents/123"
                  }
                }
              }
            }
          },
          "assigned_to": {
            "$ref": "#/components/schemas/SafetyHubPersonRef"
          },
          "assignee_name": {
            "type": "string",
            "description": "Display string — assignee full name, a free-text role, or \"Unassigned\"."
          },
          "location": {
            "$ref": "#/components/schemas/SafetyHubLocationRef"
          },
          "due_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "overdue": {
            "type": "boolean",
            "description": "True for an open action past its due date."
          },
          "days_overdue": {
            "type": "integer",
            "description": "Days past due for an overdue action, else 0."
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "resolution_notes": {
            "type": "string",
            "nullable": true
          },
          "verified": {
            "type": "boolean",
            "description": "ISO 45001 effectiveness-verification state."
          },
          "verified_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SafetyHubCorrectiveActionDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SafetyHubCorrectiveAction"
          },
          {
            "type": "object",
            "description": "The GET /safety_hub/corrective_actions/{id} detail payload — the base\ncard fields plus the show-screen additions: the ISO 45001\neffectiveness-verification record and the due-date countdown. The\nCompletion card (\"Completed on … / Closed out by …\") reads the base\n`completed_at` and `assignee_name`; the register has no separate\ncompleter, so no `completed_by` field is emitted.\n",
            "properties": {
              "verified_by": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SafetyHubPersonRef"
                  }
                ],
                "description": "The manager who signed off effectiveness; null until verified."
              },
              "effectiveness_notes": {
                "type": "string",
                "nullable": true,
                "description": "The attestation of what was checked and why the action was effective."
              },
              "days_until_due": {
                "type": "integer",
                "nullable": true,
                "description": "Days until the due date (0 if due today or past, null when no due date)."
              }
            }
          }
        ]
      },
      "SafetyHubCorrectiveActionPermissions": {
        "type": "object",
        "description": "Viewer-relative capability flags returned alongside a corrective-action\ndetail, so a native client renders the right controls without a second\nround trip. Each mirrors the desktop authority exactly.\n",
        "properties": {
          "is_assignee": {
            "type": "boolean",
            "description": "True when the caller is the action's assignee."
          },
          "is_manager": {
            "type": "boolean",
            "description": "True when the caller is a safety-hub manager (manager-or-above OR the app admin)."
          },
          "can_complete": {
            "type": "boolean",
            "description": "True when the action is OPEN and the caller is the assignee or a manager."
          },
          "can_assign": {
            "type": "boolean",
            "description": "True when the caller is a manager and the action is OPEN (offer the Assign control)."
          },
          "can_verify": {
            "type": "boolean",
            "description": "True when the tenant's effectiveness-verification policy is on, the\naction is completed and not yet verified, and the caller is a manager\nwho is NOT the assignee (self-verification is refused).\n"
          }
        }
      },
      "SafetyHubPermit": {
        "type": "object",
        "description": "One row from GET /safety_hub/permits — a permit to work (WorkPermit),\nshaped for the mobile \"My Permits\" list card. `display_state` is the\nboard state (\"active\"/\"overrun\" for an issued permit inside/past its\nwindow, else the raw status) so the client colours the status pill the\nsame way the web board does.\n",
        "properties": {
          "id": {
            "type": "integer"
          },
          "permit_number": {
            "type": "string",
            "example": "PTW-2026-0042"
          },
          "title": {
            "type": "string"
          },
          "permit_type": {
            "type": "string",
            "description": "Raw type key.",
            "enum": [
              "hot_work",
              "confined_space",
              "working_at_height",
              "electrical",
              "excavation",
              "lifting",
              "general"
            ]
          },
          "type_label": {
            "type": "string",
            "description": "Display label for the permit type (e.g. \"Hot work\")."
          },
          "status": {
            "type": "string",
            "description": "Raw lifecycle status.",
            "enum": [
              "draft",
              "requested",
              "approved",
              "suspended",
              "closed",
              "cancelled",
              "expired"
            ]
          },
          "display_state": {
            "type": "string",
            "description": "Board state — the raw status, except an issued permit reads\n`active` inside its work window and `overrun` past it.\n"
          },
          "starts_at": {
            "type": "string",
            "format": "date-time"
          },
          "ends_at": {
            "type": "string",
            "format": "date-time"
          },
          "area": {
            "type": "string",
            "nullable": true,
            "description": "Free-text area within the site (e.g. \"Bay 4\")."
          },
          "location": {
            "$ref": "#/components/schemas/SafetyHubLocationRef"
          },
          "vendor": {
            "type": "object",
            "nullable": true,
            "description": "The contractor/vendor performing the work, when set.",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          }
        }
      },
      "SafetyHubPermitDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SafetyHubPermit"
          },
          {
            "type": "object",
            "description": "The GET /safety_hub/permits/{id} detail payload — the base card\nfields plus the show-page detail: requester (issuer) / authoriser\n(approver), the type's hazards, the precaution checklist with each\ncontrol's confirmed state, and the closure record.\n",
            "properties": {
              "description": {
                "type": "string",
                "nullable": true
              },
              "requested_by": {
                "$ref": "#/components/schemas/SafetyHubPersonRef"
              },
              "approved_by": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SafetyHubPersonRef"
                  }
                ],
                "description": "The authoriser who issued the permit; null until approved."
              },
              "approved_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "approval_notes": {
                "type": "string",
                "nullable": true
              },
              "hazards": {
                "type": "array",
                "description": "The confirmed hazards for this permit.",
                "items": {
                  "type": "string"
                }
              },
              "precautions": {
                "type": "array",
                "description": "The permit type's full precaution checklist, each with the\ncontrol text and whether it has been confirmed.\n",
                "items": {
                  "type": "object",
                  "properties": {
                    "text": {
                      "type": "string"
                    },
                    "confirmed": {
                      "type": "boolean"
                    }
                  }
                }
              },
              "precautions_complete": {
                "type": "boolean",
                "description": "True when every required precaution has been confirmed."
              },
              "isolations": {
                "type": "string",
                "nullable": true
              },
              "ppe": {
                "type": "string",
                "nullable": true
              },
              "gas_test": {
                "type": "string",
                "nullable": true
              },
              "emergency_arrangements": {
                "type": "string",
                "nullable": true
              },
              "suspended_reason": {
                "type": "string",
                "nullable": true
              },
              "cancelled_reason": {
                "type": "string",
                "nullable": true
              },
              "area_left_safe": {
                "type": "boolean",
                "description": "Confirmed at close-out that the area was left safe."
              },
              "closure_notes": {
                "type": "string",
                "nullable": true
              },
              "closed_by": {
                "$ref": "#/components/schemas/SafetyHubPersonRef"
              },
              "closed_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "contractor_prequalification": {
                "nullable": true,
                "description": "The permit's \"Contractor\" card. Null for an own-crew permit (no\nvendor). When a vendor is present the block is always returned so\na client can render the \"no current pre-qualification\" (and, when\n`required`, \"cannot be issued\") state without a second call.\n",
                "type": "object",
                "properties": {
                  "required": {
                    "type": "boolean",
                    "description": "Whether the tenant requires a current pre-qualification to issue a contractor permit."
                  },
                  "current": {
                    "type": "boolean",
                    "description": "Whether a live (approved/conditional, unexpired) pre-qualification exists for the vendor."
                  },
                  "status": {
                    "type": "string",
                    "nullable": true,
                    "description": "Pre-qualification status (draft/approved/conditional/…); null when there is none."
                  },
                  "risk_tier": {
                    "type": "string",
                    "nullable": true
                  },
                  "valid_until": {
                    "type": "string",
                    "format": "date",
                    "nullable": true
                  },
                  "conditions": {
                    "type": "string",
                    "nullable": true
                  }
                }
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        ]
      },
      "SafetyHubKbCategoryFacet": {
        "type": "object",
        "description": "One category chip from GET /safety_hub/knowledge_base — a filter value,\nits display label, and the un-paginated number of articles behind it.\nThe `all` facet carries the total across every category.\n",
        "properties": {
          "value": {
            "type": "string",
            "example": "ppe_guides",
            "description": "The category filter value (or `all` for the total chip)."
          },
          "label": {
            "type": "string",
            "example": "PPE Guides"
          },
          "count": {
            "type": "integer",
            "description": "Un-paginated number of articles in this category (respecting the source_type filter)."
          }
        }
      },
      "SafetyHubKnowledgeBaseEntry": {
        "type": "object",
        "description": "One row from GET /safety_hub/knowledge_base — a safety knowledge base\narticle (KnowledgeBaseEntry, domain :safety) shaped for the mobile\n\"Knowledge Base\" list card: source type + display label, title, the FAQ\nquestion as the card subtitle, category + display label, status, author,\nand timestamps.\n",
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "source_type": {
            "type": "string",
            "description": "Raw content-type key.",
            "enum": [
              "faq",
              "document",
              "url",
              "video"
            ]
          },
          "source_type_label": {
            "type": "string",
            "description": "Display label for the source type (e.g. \"Safety FAQ\", \"Document\")."
          },
          "category": {
            "type": "string",
            "description": "Raw category key."
          },
          "category_label": {
            "type": "string",
            "description": "Display label for the category (e.g. \"PPE Guides\")."
          },
          "question": {
            "type": "string",
            "nullable": true,
            "description": "The FAQ question, shown as the card subtitle; null for non-FAQ types."
          },
          "status": {
            "type": "string",
            "description": "Lifecycle status. A member only ever sees `active`; a manager also\nsees `draft`, `processing`, `failed`, `archived`, `pending_review`.\n",
            "enum": [
              "draft",
              "processing",
              "active",
              "failed",
              "archived",
              "pending_review"
            ]
          },
          "created_by": {
            "$ref": "#/components/schemas/SafetyHubPersonRef"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SafetyHubKnowledgeBaseEntryDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SafetyHubKnowledgeBaseEntry"
          },
          {
            "type": "object",
            "description": "The GET /safety_hub/knowledge_base/{id} detail payload — the base\ncard fields plus the show-page detail: the answer / extracted body,\nany additional Q&A pairs, a safe external URL, and attached-file\nmetadata.\n",
            "properties": {
              "answer": {
                "type": "string",
                "nullable": true,
                "description": "The FAQ answer; null for non-FAQ types."
              },
              "content": {
                "type": "string",
                "nullable": true,
                "description": "Extracted / crawled / transcribed body for document, url and video entries."
              },
              "description": {
                "type": "string",
                "nullable": true
              },
              "faq_items": {
                "type": "array",
                "description": "Additional Q&A pairs beyond the primary question/answer (empty for legacy column-authored FAQs).",
                "items": {
                  "type": "object",
                  "properties": {
                    "question": {
                      "type": "string"
                    },
                    "answer": {
                      "type": "string"
                    }
                  }
                }
              },
              "source_url": {
                "type": "string",
                "nullable": true,
                "description": "A safe absolute http(s) URL for url-sourced entries; null otherwise (re-validated at the sink)."
              },
              "file": {
                "type": "object",
                "nullable": true,
                "description": "Attached-file metadata for a document/video entry; null when nothing is attached.",
                "properties": {
                  "filename": {
                    "type": "string"
                  },
                  "content_type": {
                    "type": "string"
                  },
                  "byte_size": {
                    "type": "integer"
                  },
                  "url": {
                    "type": "string",
                    "description": "Absolute download URL with a 1-hour signed expiry."
                  }
                }
              },
              "minimum_role": {
                "type": "integer",
                "description": "Role floor for access (0=super_admin … 4=member)."
              }
            }
          }
        ]
      },
      "SafetyHubSubmission": {
        "type": "object",
        "description": "One row from GET /safety_hub/submissions — UI-ready metadata.",
        "properties": {
          "id": {
            "type": "integer",
            "description": "ID of the underlying incident or observation"
          },
          "type": {
            "type": "object",
            "description": "Kind of submission plus UI metadata (icon name + color token)",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "incident",
                  "observation"
                ]
              },
              "label": {
                "type": "string",
                "example": "Incident"
              },
              "icon": {
                "type": "string",
                "example": "alert-triangle",
                "description": "Stable icon-name token (client maps to its icon set)"
              },
              "icon_color": {
                "type": "string",
                "example": "danger",
                "description": "Bootstrap color token: success / warning / danger / info / primary / dark / secondary"
              }
            }
          },
          "summary": {
            "type": "string",
            "description": "Headline — incident title or truncated observation description"
          },
          "category": {
            "type": "string",
            "nullable": true,
            "description": "incident_type for incidents, observation category for observations"
          },
          "severity": {
            "type": "object",
            "description": "Severity (incidents) or observation_type (observations) with display label and color",
            "properties": {
              "value": {
                "type": "string",
                "nullable": true
              },
              "label": {
                "type": "string",
                "nullable": true
              },
              "color": {
                "type": "string",
                "description": "Bootstrap color token"
              }
            }
          },
          "location": {
            "type": "object",
            "nullable": true,
            "properties": {
              "name": {
                "type": "string"
              }
            }
          },
          "date": {
            "type": "string",
            "format": "date-time",
            "description": "Event date — `occurred_at` (incidents) or `observed_at`\n(observations). Note: this is NOT the sort key. The feed is\nordered by `submitted_at` desc.\n"
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the row was filed (record `created_at`). This is the sort\nkey for the feed (descending — most recent submissions first).\n"
          },
          "status": {
            "type": "object",
            "description": "Status with display label + color. For observations needing follow-up,\nthe value is the synthetic `follow_up_required` (color `warning`)\ninstead of the raw enum value — matching the desktop badge override.\n",
            "properties": {
              "value": {
                "type": "string",
                "nullable": true
              },
              "label": {
                "type": "string",
                "nullable": true
              },
              "color": {
                "type": "string"
              }
            }
          },
          "anonymous": {
            "type": "boolean",
            "description": "Whether the row was filed anonymously. Always `false` in the\npersonal feed (anonymous rows are filtered upstream). May be\n`true` in the team feed (`team=true`), which includes anonymous\nrows so managers can triage them.\n"
          },
          "submitter": {
            "type": "object",
            "nullable": true,
            "description": "The user who filed the row — `reporter` for incidents,\n`observer` for observations. `null` when `anonymous: true`\nso the team feed doesn't leak the identity of anonymous\nreporters. `null` is also possible if the underlying user\nrecord has been removed.\n",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string",
                "description": "User's full name"
              }
            }
          },
          "investigator": {
            "type": "object",
            "nullable": true,
            "description": "Assigned investigator for incident rows\n(`Incident.assigned_investigator`). Always `null` for\nobservation rows — observations have no investigator\nconcept. Also `null` for incidents that have not yet had\nan investigator assigned.\n",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string",
                "description": "User's full name"
              }
            }
          },
          "due_date": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Investigation due date for incident rows\n(`Incident.investigation_due_at`). Always `null` for\nobservation rows — observations have no due-date concept.\nAlso `null` for incidents whose investigation has not\nbeen scheduled with a deadline.\n"
          },
          "url": {
            "type": "string",
            "description": "Deep-link to the desktop record (e.g., `/apps/safety-hub/incidents/123`)"
          }
        }
      },
      "SafetyHubSubmissionsSummary": {
        "type": "object",
        "description": "Headline tile counts shown above the submissions feed. Scope follows\nthe `team` query parameter so the tiles always match the rows in the\nfeed:\n  * `team` absent or `false` — the **current user's** own submissions\n    this calendar month, with anonymous rows excluded.\n  * `team=true` — **every submission in the business** this calendar\n    month, including anonymous rows (manager-gated, same gate as the\n    feed itself).\nDisabled per-module toggles (`incidents_enabled`,\n`observations_enabled`) contribute `0` in both scopes.\n",
        "properties": {
          "this_month": {
            "type": "object",
            "description": "Total submissions (incidents + observations) this calendar month, scoped per the `team` parameter",
            "properties": {
              "label": {
                "type": "string",
                "example": "This Month"
              },
              "count": {
                "type": "integer",
                "example": 6
              },
              "color": {
                "type": "string",
                "example": "info",
                "description": "Bootstrap color token: success / warning / danger / info / primary / dark / secondary"
              },
              "icon": {
                "type": "string",
                "example": "calendar",
                "description": "Stable icon-name token (client maps to its icon set)"
              }
            }
          },
          "incidents": {
            "type": "object",
            "description": "Incident submissions this calendar month, scoped per the `team` parameter",
            "properties": {
              "label": {
                "type": "string",
                "example": "Incidents"
              },
              "count": {
                "type": "integer",
                "example": 3
              },
              "color": {
                "type": "string",
                "example": "danger"
              },
              "icon": {
                "type": "string",
                "example": "alert-triangle"
              }
            }
          },
          "observations": {
            "type": "object",
            "description": "Observation submissions this calendar month, scoped per the `team` parameter",
            "properties": {
              "label": {
                "type": "string",
                "example": "Observations"
              },
              "count": {
                "type": "integer",
                "example": 3
              },
              "color": {
                "type": "string",
                "example": "primary"
              },
              "icon": {
                "type": "string",
                "example": "eye"
              }
            }
          }
        }
      },
      "Holiday": {
        "type": "object",
        "description": "Business holiday information",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique holiday ID",
            "example": 1
          },
          "name": {
            "type": "string",
            "description": "Holiday name",
            "example": "New Year's Day"
          },
          "date": {
            "type": "string",
            "format": "date",
            "description": "Holiday date",
            "example": "2024-01-01"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "Holiday description",
            "example": "New Year's Day celebration"
          },
          "recurring": {
            "type": "boolean",
            "description": "Whether this holiday recurs annually",
            "example": true
          },
          "business_id": {
            "type": "integer",
            "description": "Business ID",
            "example": 1
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2024-01-01T00:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2024-01-01T00:00:00Z"
          }
        },
        "required": [
          "id",
          "name",
          "date",
          "business_id",
          "created_at",
          "updated_at"
        ]
      },
      "HolidayDetailed": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Holiday"
          },
          {
            "type": "object",
            "properties": {
              "day_of_week": {
                "type": "string",
                "description": "Day of the week",
                "example": "Monday"
              },
              "week_of_year": {
                "type": "integer",
                "description": "Week number of the year",
                "example": 1
              },
              "month_name": {
                "type": "string",
                "description": "Month name",
                "example": "January"
              },
              "is_weekend": {
                "type": "boolean",
                "description": "Whether the holiday falls on a weekend",
                "example": false
              },
              "days_until": {
                "type": "integer",
                "nullable": true,
                "description": "Days until this holiday (null if past)",
                "example": 30
              },
              "metadata": {
                "type": "object",
                "description": "Additional holiday metadata",
                "properties": {
                  "year": {
                    "type": "integer",
                    "example": 2024
                  },
                  "month": {
                    "type": "integer",
                    "example": 1
                  },
                  "day": {
                    "type": "integer",
                    "example": 1
                  },
                  "quarter": {
                    "type": "integer",
                    "example": 1
                  },
                  "is_past": {
                    "type": "boolean",
                    "example": false
                  },
                  "is_today": {
                    "type": "boolean",
                    "example": false
                  },
                  "is_future": {
                    "type": "boolean",
                    "example": true
                  }
                }
              }
            }
          }
        ]
      },
      "BlackoutPeriod": {
        "type": "object",
        "description": "Leave blackout period information",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique blackout period ID",
            "example": 1
          },
          "name": {
            "type": "string",
            "description": "Blackout period name",
            "example": "Winter Holiday Season"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "Blackout period description",
            "example": "No leave allowed during winter holidays"
          },
          "start_date": {
            "type": "string",
            "format": "date",
            "description": "Start date of blackout period",
            "example": "2024-12-15"
          },
          "end_date": {
            "type": "string",
            "format": "date",
            "description": "End date of blackout period",
            "example": "2024-12-31"
          },
          "block_requests": {
            "type": "boolean",
            "description": "Whether requests are completely blocked or require special approval",
            "example": true
          },
          "status": {
            "type": "string",
            "description": "Current status of the blackout period",
            "example": "upcoming"
          },
          "duration_days": {
            "type": "integer",
            "description": "Duration in days",
            "example": 17
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2024-01-01T00:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2024-01-01T00:00:00Z"
          }
        },
        "required": [
          "id",
          "name",
          "start_date",
          "end_date",
          "block_requests",
          "status",
          "duration_days"
        ]
      },
      "BlackoutPeriodDetailed": {
        "allOf": [
          {
            "$ref": "#/components/schemas/BlackoutPeriod"
          },
          {
            "type": "object",
            "properties": {
              "leave_types": {
                "type": "array",
                "description": "Leave types affected by this blackout",
                "items": {
                  "$ref": "#/components/schemas/LeaveTypeBasic"
                }
              },
              "locations": {
                "type": "array",
                "description": "Locations affected by this blackout",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer",
                      "example": 1
                    },
                    "name": {
                      "type": "string",
                      "example": "Main Office"
                    },
                    "address": {
                      "type": "string",
                      "example": "123 Main St"
                    }
                  }
                }
              },
              "applies_to_all_leave_types": {
                "type": "boolean",
                "description": "Whether this applies to all leave types",
                "example": true
              },
              "applies_to_all_locations": {
                "type": "boolean",
                "description": "Whether this applies to all locations",
                "example": true
              },
              "days_until_start": {
                "type": "integer",
                "nullable": true,
                "description": "Days until blackout starts (null if past)",
                "example": 30
              },
              "days_until_end": {
                "type": "integer",
                "nullable": true,
                "description": "Days until blackout ends (null if past)",
                "example": 47
              },
              "is_current": {
                "type": "boolean",
                "description": "Whether this blackout is currently active",
                "example": false
              },
              "is_upcoming": {
                "type": "boolean",
                "description": "Whether this blackout is upcoming",
                "example": true
              },
              "is_past": {
                "type": "boolean",
                "description": "Whether this blackout is in the past",
                "example": false
              },
              "created_by": {
                "type": "object",
                "nullable": true,
                "description": "User who created this blackout period",
                "properties": {
                  "id": {
                    "type": "integer",
                    "example": 1
                  },
                  "name": {
                    "type": "string",
                    "example": "Admin User"
                  }
                }
              },
              "impact_message": {
                "type": "string",
                "description": "Message describing the impact of this blackout",
                "example": "Leave requests are completely blocked during this period"
              }
            }
          }
        ]
      },
      "LeaveCoverageAvailability": {
        "type": "object",
        "description": "Leave availability check result",
        "properties": {
          "date": {
            "type": "string",
            "format": "date",
            "description": "Date checked",
            "example": "2024-03-15"
          },
          "available": {
            "type": "boolean",
            "description": "Whether leave is available",
            "example": true
          },
          "unlimited": {
            "type": "boolean",
            "description": "Whether coverage is unlimited",
            "example": false
          },
          "current_off": {
            "type": "integer",
            "description": "Number of people currently off",
            "example": 2
          },
          "max_allowed": {
            "type": "integer",
            "nullable": true,
            "description": "Maximum people allowed off (null if unlimited)",
            "example": 5
          },
          "available_spots": {
            "type": "integer",
            "nullable": true,
            "description": "Available spots remaining (null if unlimited)",
            "example": 3
          },
          "limit_type": {
            "type": "string",
            "description": "Type of coverage limit",
            "example": "percentage"
          },
          "message": {
            "type": "string",
            "description": "Human-readable availability message",
            "example": "3 spots available out of 5 maximum"
          },
          "metadata": {
            "type": "object",
            "description": "Additional date metadata",
            "properties": {
              "day_of_week": {
                "type": "string",
                "example": "Friday"
              },
              "is_weekend": {
                "type": "boolean",
                "example": false
              },
              "is_holiday": {
                "type": "boolean",
                "example": false
              },
              "is_blackout_period": {
                "type": "boolean",
                "example": false
              }
            }
          }
        },
        "required": [
          "date",
          "available",
          "unlimited",
          "current_off",
          "message",
          "metadata"
        ]
      },
      "LeaveCoverageDateRange": {
        "type": "object",
        "description": "Leave coverage for date range",
        "properties": {
          "start_date": {
            "type": "string",
            "format": "date",
            "example": "2024-03-15"
          },
          "end_date": {
            "type": "string",
            "format": "date",
            "example": "2024-03-17"
          },
          "total_days": {
            "type": "integer",
            "description": "Total days in range",
            "example": 3
          },
          "coverage_data": {
            "type": "array",
            "description": "Coverage data for each day",
            "items": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/LeaveCoverageAvailability"
                },
                {
                  "type": "object",
                  "properties": {
                    "week_of_year": {
                      "type": "integer",
                      "example": 11
                    },
                    "month": {
                      "type": "integer",
                      "example": 3
                    }
                  }
                }
              ]
            }
          },
          "summary": {
            "type": "object",
            "description": "Summary statistics for the range",
            "properties": {
              "total_days": {
                "type": "integer",
                "example": 3
              },
              "available_days": {
                "type": "integer",
                "example": 2
              },
              "blocked_days": {
                "type": "integer",
                "example": 1
              },
              "weekend_days": {
                "type": "integer",
                "example": 0
              },
              "holiday_days": {
                "type": "integer",
                "example": 0
              },
              "blackout_days": {
                "type": "integer",
                "example": 1
              },
              "availability_percentage": {
                "type": "number",
                "example": 66.7
              }
            }
          }
        },
        "required": [
          "start_date",
          "end_date",
          "total_days",
          "coverage_data",
          "summary"
        ]
      },
      "LeaveCoverageAlternatives": {
        "type": "object",
        "description": "Alternative date suggestions",
        "properties": {
          "original_request": {
            "type": "object",
            "description": "Original requested dates",
            "properties": {
              "start_date": {
                "type": "string",
                "format": "date",
                "example": "2024-03-15"
              },
              "end_date": {
                "type": "string",
                "format": "date",
                "example": "2024-03-17"
              },
              "duration_days": {
                "type": "integer",
                "example": 3
              }
            }
          },
          "alternatives": {
            "type": "array",
            "description": "Alternative date ranges",
            "items": {
              "type": "object",
              "properties": {
                "start_date": {
                  "type": "string",
                  "format": "date",
                  "example": "2024-03-22"
                },
                "end_date": {
                  "type": "string",
                  "format": "date",
                  "example": "2024-03-24"
                },
                "duration_days": {
                  "type": "integer",
                  "example": 3
                },
                "days_from_original": {
                  "type": "integer",
                  "description": "Days difference from original start date",
                  "example": 7
                },
                "has_weekends": {
                  "type": "boolean",
                  "example": true
                },
                "has_holidays": {
                  "type": "boolean",
                  "example": false
                },
                "business_days": {
                  "type": "integer",
                  "example": 3
                }
              }
            }
          },
          "total_alternatives": {
            "type": "integer",
            "description": "Number of alternatives found",
            "example": 5
          },
          "search_period": {
            "type": "object",
            "description": "Period searched for alternatives",
            "properties": {
              "start_date": {
                "type": "string",
                "format": "date",
                "example": "2024-03-15"
              },
              "end_date": {
                "type": "string",
                "format": "date",
                "example": "2024-05-14"
              }
            }
          }
        },
        "required": [
          "original_request",
          "alternatives",
          "total_alternatives",
          "search_period"
        ]
      },
      "LeaveCoverageCalendar": {
        "type": "object",
        "description": "Monthly calendar view of leave coverage",
        "properties": {
          "year": {
            "type": "integer",
            "example": 2024
          },
          "month": {
            "type": "integer",
            "example": 3
          },
          "month_name": {
            "type": "string",
            "example": "March"
          },
          "days_in_month": {
            "type": "integer",
            "example": 31
          },
          "calendar_data": {
            "type": "array",
            "description": "Data for each day of the month",
            "items": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/LeaveCoverageAvailability"
                },
                {
                  "type": "object",
                  "properties": {
                    "day_of_month": {
                      "type": "integer",
                      "example": 15
                    },
                    "day_of_week_short": {
                      "type": "string",
                      "example": "Fri"
                    },
                    "is_today": {
                      "type": "boolean",
                      "example": false
                    },
                    "holiday": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 1
                        },
                        "name": {
                          "type": "string",
                          "example": "Good Friday"
                        },
                        "description": {
                          "type": "string",
                          "example": "Christian holiday"
                        }
                      }
                    },
                    "blackout_period": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 1
                        },
                        "name": {
                          "type": "string",
                          "example": "Spring Break"
                        },
                        "blocks_requests": {
                          "type": "boolean",
                          "example": true
                        }
                      }
                    },
                    "week_of_month": {
                      "type": "integer",
                      "example": 3
                    }
                  }
                }
              ]
            }
          },
          "summary": {
            "type": "object",
            "description": "Monthly summary statistics",
            "properties": {
              "total_days": {
                "type": "integer",
                "example": 31
              },
              "available_days": {
                "type": "integer",
                "example": 25
              },
              "blocked_days": {
                "type": "integer",
                "example": 6
              },
              "weekend_days": {
                "type": "integer",
                "example": 8
              },
              "holiday_days": {
                "type": "integer",
                "example": 2
              },
              "blackout_days": {
                "type": "integer",
                "example": 4
              }
            }
          }
        },
        "required": [
          "year",
          "month",
          "month_name",
          "days_in_month",
          "calendar_data",
          "summary"
        ]
      },
      "LeaveRequestHistory": {
        "allOf": [
          {
            "$ref": "#/components/schemas/LeaveRequestDetailed"
          },
          {
            "type": "object",
            "properties": {
              "status_history": {
                "type": "array",
                "description": "Complete status change history",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer",
                      "example": 1
                    },
                    "from_status": {
                      "type": "string",
                      "example": "pending"
                    },
                    "to_status": {
                      "type": "string",
                      "example": "approved"
                    },
                    "changed_by": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 2
                        },
                        "name": {
                          "type": "string",
                          "example": "Manager Name"
                        }
                      }
                    },
                    "notes": {
                      "type": "string",
                      "nullable": true,
                      "example": "Approved for vacation"
                    },
                    "changed_at": {
                      "type": "string",
                      "format": "date-time",
                      "example": "2024-02-16T14:30:00Z"
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "LeaveAnalytics": {
        "type": "object",
        "description": "Comprehensive leave usage analytics",
        "properties": {
          "period": {
            "type": "object",
            "description": "Analytics period",
            "properties": {
              "start_date": {
                "type": "string",
                "format": "date",
                "example": "2024-01-01"
              },
              "end_date": {
                "type": "string",
                "format": "date",
                "example": "2024-12-31"
              },
              "total_days": {
                "type": "integer",
                "example": 366
              }
            }
          },
          "summary": {
            "type": "object",
            "description": "Overall summary statistics",
            "properties": {
              "total_requests": {
                "type": "integer",
                "example": 25
              },
              "total_leave_days": {
                "type": "integer",
                "example": 75
              },
              "approved_requests": {
                "type": "integer",
                "example": 20
              },
              "approved_days": {
                "type": "integer",
                "example": 60
              },
              "pending_requests": {
                "type": "integer",
                "example": 3
              },
              "denied_requests": {
                "type": "integer",
                "example": 1
              },
              "cancelled_requests": {
                "type": "integer",
                "example": 1
              }
            }
          },
          "by_leave_type": {
            "type": "array",
            "description": "Analytics grouped by leave type",
            "items": {
              "type": "object",
              "properties": {
                "leave_type": {
                  "$ref": "#/components/schemas/LeaveTypeBasic"
                },
                "total_requests": {
                  "type": "integer",
                  "example": 15
                },
                "total_days": {
                  "type": "integer",
                  "example": 45
                },
                "approved_requests": {
                  "type": "integer",
                  "example": 12
                },
                "approved_days": {
                  "type": "integer",
                  "example": 36
                },
                "pending_requests": {
                  "type": "integer",
                  "example": 2
                },
                "denied_requests": {
                  "type": "integer",
                  "example": 1
                },
                "cancelled_requests": {
                  "type": "integer",
                  "example": 0
                }
              }
            }
          },
          "by_month": {
            "type": "array",
            "description": "Analytics grouped by month",
            "items": {
              "type": "object",
              "properties": {
                "month": {
                  "type": "string",
                  "example": "2024-03"
                },
                "month_name": {
                  "type": "string",
                  "example": "March 2024"
                },
                "total_requests": {
                  "type": "integer",
                  "example": 5
                },
                "total_days": {
                  "type": "integer",
                  "example": 15
                },
                "approved_days": {
                  "type": "integer",
                  "example": 12
                }
              }
            }
          },
          "by_status": {
            "type": "array",
            "description": "Analytics grouped by status",
            "items": {
              "type": "object",
              "properties": {
                "status": {
                  "type": "string",
                  "example": "approved"
                },
                "count": {
                  "type": "integer",
                  "example": 20
                },
                "total_days": {
                  "type": "integer",
                  "example": 60
                },
                "percentage": {
                  "type": "number",
                  "example": 80.0
                }
              }
            }
          },
          "usage_patterns": {
            "type": "object",
            "description": "Usage patterns and insights",
            "properties": {
              "most_used_leave_type": {
                "type": "object",
                "nullable": true,
                "description": "Leave type with most usage"
              },
              "busiest_month": {
                "type": "object",
                "nullable": true,
                "description": "Month with most leave days"
              },
              "average_request_length": {
                "type": "number",
                "description": "Average length of leave requests in days",
                "example": 3.0
              },
              "longest_request": {
                "type": "object",
                "nullable": true,
                "description": "Longest leave request"
              },
              "approval_rate": {
                "type": "number",
                "description": "Percentage of requests approved",
                "example": 80.0
              }
            }
          }
        },
        "required": [
          "period",
          "summary",
          "by_leave_type",
          "by_month",
          "by_status",
          "usage_patterns"
        ]
      },
      "BulkLeaveRequestResponse": {
        "type": "object",
        "description": "Response from bulk leave request operation",
        "properties": {
          "results": {
            "type": "array",
            "description": "Successful requests",
            "items": {
              "type": "object",
              "properties": {
                "index": {
                  "type": "integer",
                  "description": "Index of request in original array",
                  "example": 0
                },
                "success": {
                  "type": "boolean",
                  "example": true
                },
                "leave_request": {
                  "$ref": "#/components/schemas/LeaveRequestDetailed"
                }
              }
            }
          },
          "errors": {
            "type": "array",
            "description": "Failed requests",
            "items": {
              "type": "object",
              "properties": {
                "index": {
                  "type": "integer",
                  "description": "Index of request in original array",
                  "example": 1
                },
                "success": {
                  "type": "boolean",
                  "example": false
                },
                "errors": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "example": [
                    "Start date cannot be in the past"
                  ]
                }
              }
            }
          },
          "summary": {
            "type": "object",
            "description": "Summary of bulk operation",
            "properties": {
              "total_requests": {
                "type": "integer",
                "example": 5
              },
              "successful": {
                "type": "integer",
                "example": 4
              },
              "failed": {
                "type": "integer",
                "example": 1
              }
            }
          }
        },
        "required": [
          "results",
          "errors",
          "summary"
        ]
      },
      "Timesheet": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 123
          },
          "start_date": {
            "type": "string",
            "format": "date",
            "example": "2024-01-15"
          },
          "end_date": {
            "type": "string",
            "format": "date",
            "example": "2024-01-21"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "submitted",
              "regional_pending",
              "approved",
              "rejected"
            ],
            "example": "pending"
          },
          "total_hours": {
            "type": "number",
            "format": "float",
            "example": 40.0
          },
          "regular_hours": {
            "type": "number",
            "format": "float",
            "example": 40.0
          },
          "overtime_hours": {
            "type": "number",
            "format": "float",
            "example": 0.0
          },
          "submission_date": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": "2024-01-22T09:00:00Z"
          },
          "approval_date": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": "2024-01-22T14:30:00Z"
          },
          "approved_by": {
            "type": "string",
            "nullable": true,
            "example": "John Manager"
          },
          "regional_approved": {
            "type": "boolean",
            "example": false
          },
          "regional_approved_by": {
            "type": "string",
            "nullable": true,
            "example": "Jane Regional"
          },
          "regional_approved_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": "2024-01-22T16:00:00Z"
          },
          "editable": {
            "type": "boolean",
            "description": "Whether the timesheet can be edited",
            "example": true
          },
          "submittable": {
            "type": "boolean",
            "description": "Whether the timesheet can be submitted",
            "example": true
          },
          "entries_count": {
            "type": "integer",
            "description": "Number of entries in this timesheet",
            "example": 5
          },
          "missing_punches_count": {
            "type": "integer",
            "description": "Number of entries with missing punches",
            "example": 0
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2024-01-15T00:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2024-01-21T18:00:00Z"
          }
        }
      },
      "TimesheetDetailed": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Timesheet"
          },
          {
            "type": "object",
            "properties": {
              "entries": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TimesheetEntry"
                }
              },
              "entries_by_date": {
                "type": "object",
                "description": "Entries grouped by date",
                "additionalProperties": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/TimesheetEntry"
                  }
                }
              },
              "summary": {
                "$ref": "#/components/schemas/TimesheetSummary"
              }
            }
          }
        ]
      },
      "TimesheetEntry": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 456
          },
          "date": {
            "type": "string",
            "format": "date",
            "example": "2024-01-15"
          },
          "start_time": {
            "type": "string",
            "format": "time",
            "nullable": true,
            "example": "09:00:00"
          },
          "end_time": {
            "type": "string",
            "format": "time",
            "nullable": true,
            "example": "17:00:00"
          },
          "hours": {
            "type": "number",
            "format": "float",
            "nullable": true,
            "example": 8.0
          },
          "edited": {
            "type": "boolean",
            "description": "Whether this entry has been manually edited",
            "example": false
          },
          "edit_notes": {
            "type": "string",
            "nullable": true,
            "description": "Notes about edits made to this entry",
            "example": "Corrected clock-in time"
          },
          "missing_punch": {
            "type": "boolean",
            "description": "Whether this entry is missing clock-in or clock-out",
            "example": false
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "approved",
              "rejected"
            ],
            "example": "pending"
          },
          "attendance_record_id": {
            "type": "integer",
            "nullable": true,
            "description": "Associated attendance record ID",
            "example": 789
          },
          "shift_id": {
            "type": "integer",
            "nullable": true,
            "description": "Associated shift ID",
            "example": 101
          },
          "shift": {
            "type": "object",
            "nullable": true,
            "description": "Associated shift information",
            "properties": {
              "id": {
                "type": "integer",
                "example": 101
              },
              "title": {
                "type": "string",
                "example": "Morning Shift"
              },
              "location": {
                "type": "string",
                "nullable": true,
                "example": "Main Office"
              },
              "is_ad_hoc": {
                "type": "boolean",
                "example": false
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2024-01-15T09:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2024-01-15T17:00:00Z"
          }
        }
      },
      "TimesheetEntryDetailed": {
        "allOf": [
          {
            "$ref": "#/components/schemas/TimesheetEntry"
          },
          {
            "type": "object",
            "properties": {
              "timesheet": {
                "type": "object",
                "description": "Associated timesheet information",
                "properties": {
                  "id": {
                    "type": "integer",
                    "example": 123
                  },
                  "start_date": {
                    "type": "string",
                    "format": "date",
                    "example": "2024-01-15"
                  },
                  "end_date": {
                    "type": "string",
                    "format": "date",
                    "example": "2024-01-21"
                  },
                  "status": {
                    "type": "string",
                    "example": "pending"
                  }
                }
              },
              "edit_history": {
                "type": "array",
                "description": "History of edits made to this entry",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer",
                      "example": 1
                    },
                    "field_changed": {
                      "type": "string",
                      "example": "start_time"
                    },
                    "original_value": {
                      "type": "string",
                      "example": "08:45:00"
                    },
                    "new_value": {
                      "type": "string",
                      "example": "09:00:00"
                    },
                    "reason": {
                      "type": "string",
                      "example": "Employee correction"
                    },
                    "edited_by": {
                      "type": "string",
                      "example": "John Employee"
                    },
                    "edited_at": {
                      "type": "string",
                      "format": "date-time",
                      "example": "2024-01-15T10:00:00Z"
                    }
                  }
                }
              },
              "attendance_record": {
                "type": "object",
                "nullable": true,
                "description": "Associated attendance record details",
                "properties": {
                  "id": {
                    "type": "integer",
                    "example": 789
                  },
                  "check_in_time": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true,
                    "example": "2024-01-15T09:00:00Z"
                  },
                  "check_out_time": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true,
                    "example": "2024-01-15T17:00:00Z"
                  },
                  "status": {
                    "type": "string",
                    "example": "completed"
                  },
                  "requires_review": {
                    "type": "boolean",
                    "example": false
                  }
                }
              }
            }
          }
        ]
      },
      "TimesheetSummary": {
        "type": "object",
        "properties": {
          "total_hours": {
            "type": "number",
            "format": "float",
            "example": 40.0
          },
          "regular_hours": {
            "type": "number",
            "format": "float",
            "example": 40.0
          },
          "overtime_hours": {
            "type": "number",
            "format": "float",
            "example": 0.0
          },
          "total_entries": {
            "type": "integer",
            "example": 5
          },
          "missing_punches": {
            "type": "integer",
            "example": 0
          },
          "edited_entries": {
            "type": "integer",
            "example": 1
          },
          "days_with_entries": {
            "type": "integer",
            "example": 5
          }
        }
      },
      "PaySummary": {
        "type": "object",
        "properties": {
          "pay_period": {
            "type": "object",
            "properties": {
              "start_date": {
                "type": "string",
                "format": "date",
                "example": "2024-01-15"
              },
              "end_date": {
                "type": "string",
                "format": "date",
                "example": "2024-01-21"
              },
              "period_type": {
                "type": "string",
                "enum": [
                  "weekly",
                  "bi-weekly",
                  "semi-monthly",
                  "monthly"
                ],
                "example": "weekly"
              }
            }
          },
          "hours": {
            "type": "object",
            "properties": {
              "regular_hours": {
                "type": "number",
                "format": "float",
                "example": 40.0
              },
              "overtime_hours": {
                "type": "number",
                "format": "float",
                "example": 2.5
              },
              "total_hours": {
                "type": "number",
                "format": "float",
                "example": 42.5
              }
            }
          },
          "estimated_pay": {
            "type": "object",
            "properties": {
              "regular_pay": {
                "type": "number",
                "format": "float",
                "example": 1000.0
              },
              "overtime_pay": {
                "type": "number",
                "format": "float",
                "example": 93.75
              },
              "total_pay": {
                "type": "number",
                "format": "float",
                "example": 1093.75
              },
              "hourly_rate": {
                "type": "number",
                "format": "float",
                "example": 25.0
              },
              "overtime_rate": {
                "type": "number",
                "format": "float",
                "example": 37.5
              }
            }
          },
          "timesheet_status": {
            "type": "string",
            "enum": [
              "not_created",
              "pending",
              "submitted",
              "approved",
              "rejected"
            ],
            "example": "pending"
          },
          "last_updated": {
            "type": "string",
            "format": "date-time",
            "example": "2024-01-20T10:30:00Z"
          }
        }
      },
      "PayHistoryItem": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "timesheet",
              "paycheck"
            ],
            "example": "timesheet"
          },
          "id": {
            "type": "integer",
            "example": 123
          },
          "period_start": {
            "type": "string",
            "format": "date",
            "example": "2024-01-15"
          },
          "period_end": {
            "type": "string",
            "format": "date",
            "example": "2024-01-21"
          },
          "regular_hours": {
            "type": "number",
            "format": "float",
            "example": 40.0
          },
          "overtime_hours": {
            "type": "number",
            "format": "float",
            "example": 2.5
          },
          "total_hours": {
            "type": "number",
            "format": "float",
            "example": 42.5
          },
          "status": {
            "type": "string",
            "description": "Status (for timesheets)",
            "example": "approved"
          },
          "approved_at": {
            "type": "string",
            "format": "date-time",
            "description": "Approval date (for timesheets)",
            "example": "2024-01-22T14:30:00Z"
          },
          "estimated_pay": {
            "type": "number",
            "format": "float",
            "description": "Estimated pay (for timesheets)",
            "example": 1093.75
          },
          "gross_pay": {
            "type": "number",
            "format": "float",
            "description": "Gross pay (for paychecks)",
            "example": 1093.75
          },
          "net_pay": {
            "type": "number",
            "format": "float",
            "description": "Net pay (for paychecks)",
            "example": 850.0
          },
          "pay_date": {
            "type": "string",
            "format": "date",
            "description": "Pay date (for paychecks)",
            "example": "2024-01-26"
          }
        }
      },
      "PayPeriodDetailed": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "2024-01-15_2024-01-21"
          },
          "start_date": {
            "type": "string",
            "format": "date",
            "example": "2024-01-15"
          },
          "end_date": {
            "type": "string",
            "format": "date",
            "example": "2024-01-21"
          },
          "period_type": {
            "type": "string",
            "enum": [
              "weekly",
              "bi-weekly",
              "semi-monthly",
              "monthly"
            ],
            "example": "weekly"
          },
          "days_in_period": {
            "type": "integer",
            "example": 7
          },
          "is_current": {
            "type": "boolean",
            "example": true
          },
          "is_future": {
            "type": "boolean",
            "example": false
          },
          "is_past": {
            "type": "boolean",
            "example": false
          },
          "payday": {
            "type": "string",
            "format": "date",
            "example": "2024-01-26"
          },
          "days_until_payday": {
            "type": "integer",
            "description": "Days until payday (for current period)",
            "example": 5
          },
          "days_remaining": {
            "type": "integer",
            "description": "Days remaining in period (for current period)",
            "example": 2
          },
          "timesheet": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "example": 123
              },
              "status": {
                "type": "string",
                "enum": [
                  "pending",
                  "submitted",
                  "approved",
                  "rejected"
                ],
                "example": "pending"
              },
              "total_hours": {
                "type": "number",
                "format": "float",
                "example": 42.5
              },
              "regular_hours": {
                "type": "number",
                "format": "float",
                "example": 40.0
              },
              "overtime_hours": {
                "type": "number",
                "format": "float",
                "example": 2.5
              },
              "submission_date": {
                "type": "string",
                "format": "date-time",
                "example": "2024-01-21T17:00:00Z"
              },
              "approval_date": {
                "type": "string",
                "format": "date-time",
                "example": "2024-01-22T09:00:00Z"
              },
              "editable": {
                "type": "boolean",
                "example": true
              },
              "submittable": {
                "type": "boolean",
                "example": true
              }
            }
          },
          "estimated_pay": {
            "type": "number",
            "format": "float",
            "example": 1093.75
          },
          "work_days": {
            "type": "integer",
            "example": 5
          }
        }
      },
      "NextPayday": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "format": "date",
            "example": "2024-01-26"
          },
          "days_until": {
            "type": "integer",
            "example": 5
          },
          "period_start": {
            "type": "string",
            "format": "date",
            "example": "2024-01-15"
          },
          "period_end": {
            "type": "string",
            "format": "date",
            "example": "2024-01-21"
          }
        }
      },
      "YTDSummary": {
        "type": "object",
        "properties": {
          "year": {
            "type": "integer",
            "example": 2024
          },
          "period": {
            "type": "object",
            "properties": {
              "start_date": {
                "type": "string",
                "format": "date",
                "example": "2024-01-01"
              },
              "end_date": {
                "type": "string",
                "format": "date",
                "example": "2024-12-31"
              },
              "days_elapsed": {
                "type": "integer",
                "example": 20
              },
              "days_remaining": {
                "type": "integer",
                "example": 345
              }
            }
          },
          "hours": {
            "type": "object",
            "properties": {
              "regular_hours": {
                "type": "number",
                "format": "float",
                "example": 160.0
              },
              "overtime_hours": {
                "type": "number",
                "format": "float",
                "example": 10.0
              },
              "total_hours": {
                "type": "number",
                "format": "float",
                "example": 170.0
              },
              "average_weekly_hours": {
                "type": "number",
                "format": "float",
                "example": 42.5
              }
            }
          },
          "estimated_pay": {
            "type": "object",
            "properties": {
              "regular_pay": {
                "type": "number",
                "format": "float",
                "example": 4000.0
              },
              "overtime_pay": {
                "type": "number",
                "format": "float",
                "example": 375.0
              },
              "total_pay": {
                "type": "number",
                "format": "float",
                "example": 4375.0
              }
            }
          },
          "actual_pay": {
            "type": "object",
            "properties": {
              "gross_pay": {
                "type": "number",
                "format": "float",
                "example": 4200.0
              },
              "net_pay": {
                "type": "number",
                "format": "float",
                "example": 3200.0
              },
              "paychecks_count": {
                "type": "integer",
                "example": 4
              }
            }
          },
          "timesheets": {
            "type": "object",
            "properties": {
              "total_count": {
                "type": "integer",
                "example": 4
              },
              "approved_count": {
                "type": "integer",
                "example": 3
              }
            }
          }
        }
      },
      "Paycheck": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 123
          },
          "pay_date": {
            "type": "string",
            "format": "date",
            "example": "2024-01-26"
          },
          "pay_period_start": {
            "type": "string",
            "format": "date",
            "example": "2024-01-15"
          },
          "pay_period_end": {
            "type": "string",
            "format": "date",
            "example": "2024-01-21"
          },
          "regular_hours": {
            "type": "number",
            "format": "float",
            "example": 40.0
          },
          "overtime_hours": {
            "type": "number",
            "format": "float",
            "example": 2.5
          },
          "total_hours": {
            "type": "number",
            "format": "float",
            "example": 42.5
          },
          "gross_pay": {
            "type": "number",
            "format": "float",
            "example": 1093.75
          },
          "net_pay": {
            "type": "number",
            "format": "float",
            "example": 850.0
          },
          "pay_frequency": {
            "type": "string",
            "enum": [
              "weekly",
              "bi-weekly",
              "semi-monthly",
              "monthly"
            ],
            "example": "weekly"
          },
          "has_file": {
            "type": "boolean",
            "example": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2024-01-26T08:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2024-01-26T08:00:00Z"
          }
        }
      },
      "PaycheckDetailed": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Paycheck"
          },
          {
            "type": "object",
            "properties": {
              "earnings": {
                "type": "object",
                "properties": {
                  "regular_pay": {
                    "type": "number",
                    "format": "float",
                    "example": 1000.0
                  },
                  "overtime_pay": {
                    "type": "number",
                    "format": "float",
                    "example": 93.75
                  },
                  "holiday_pay": {
                    "type": "number",
                    "format": "float",
                    "example": 0.0
                  },
                  "sick_pay": {
                    "type": "number",
                    "format": "float",
                    "example": 0.0
                  },
                  "vacation_pay": {
                    "type": "number",
                    "format": "float",
                    "example": 0.0
                  },
                  "bonus": {
                    "type": "number",
                    "format": "float",
                    "example": 0.0
                  },
                  "commission": {
                    "type": "number",
                    "format": "float",
                    "example": 0.0
                  },
                  "other_earnings": {
                    "type": "number",
                    "format": "float",
                    "example": 0.0
                  },
                  "gross_pay": {
                    "type": "number",
                    "format": "float",
                    "example": 1093.75
                  }
                }
              },
              "deductions": {
                "type": "object",
                "properties": {
                  "health_insurance": {
                    "type": "number",
                    "format": "float",
                    "example": 125.0
                  },
                  "dental_insurance": {
                    "type": "number",
                    "format": "float",
                    "example": 15.0
                  },
                  "vision_insurance": {
                    "type": "number",
                    "format": "float",
                    "example": 5.0
                  },
                  "life_insurance": {
                    "type": "number",
                    "format": "float",
                    "example": 10.0
                  },
                  "retirement_401k": {
                    "type": "number",
                    "format": "float",
                    "example": 50.0
                  },
                  "retirement_roth": {
                    "type": "number",
                    "format": "float",
                    "example": 0.0
                  },
                  "hsa": {
                    "type": "number",
                    "format": "float",
                    "example": 25.0
                  },
                  "fsa": {
                    "type": "number",
                    "format": "float",
                    "example": 0.0
                  },
                  "parking": {
                    "type": "number",
                    "format": "float",
                    "example": 20.0
                  },
                  "union_dues": {
                    "type": "number",
                    "format": "float",
                    "example": 0.0
                  },
                  "other_deductions": {
                    "type": "number",
                    "format": "float",
                    "example": 0.0
                  },
                  "total_deductions": {
                    "type": "number",
                    "format": "float",
                    "example": 250.0
                  }
                }
              },
              "taxes": {
                "type": "object",
                "properties": {
                  "federal_income_tax": {
                    "type": "number",
                    "format": "float",
                    "example": 150.0
                  },
                  "state_income_tax": {
                    "type": "number",
                    "format": "float",
                    "example": 50.0
                  },
                  "local_income_tax": {
                    "type": "number",
                    "format": "float",
                    "example": 10.0
                  },
                  "social_security": {
                    "type": "number",
                    "format": "float",
                    "example": 67.81
                  },
                  "medicare": {
                    "type": "number",
                    "format": "float",
                    "example": 15.86
                  },
                  "unemployment_tax": {
                    "type": "number",
                    "format": "float",
                    "example": 0.0
                  },
                  "disability_tax": {
                    "type": "number",
                    "format": "float",
                    "example": 5.0
                  },
                  "other_taxes": {
                    "type": "number",
                    "format": "float",
                    "example": 0.0
                  },
                  "total_taxes": {
                    "type": "number",
                    "format": "float",
                    "example": 298.67
                  }
                }
              },
              "employer_contributions": {
                "type": "object",
                "properties": {
                  "health_insurance": {
                    "type": "number",
                    "format": "float",
                    "example": 200.0
                  },
                  "retirement_match": {
                    "type": "number",
                    "format": "float",
                    "example": 25.0
                  },
                  "social_security": {
                    "type": "number",
                    "format": "float",
                    "example": 67.81
                  },
                  "medicare": {
                    "type": "number",
                    "format": "float",
                    "example": 15.86
                  },
                  "unemployment": {
                    "type": "number",
                    "format": "float",
                    "example": 6.56
                  },
                  "workers_comp": {
                    "type": "number",
                    "format": "float",
                    "example": 10.94
                  },
                  "other_contributions": {
                    "type": "number",
                    "format": "float",
                    "example": 0.0
                  },
                  "total_contributions": {
                    "type": "number",
                    "format": "float",
                    "example": 326.17
                  }
                }
              },
              "year_to_date": {
                "type": "object",
                "properties": {
                  "gross_pay": {
                    "type": "number",
                    "format": "float",
                    "example": 4375.0
                  },
                  "net_pay": {
                    "type": "number",
                    "format": "float",
                    "example": 3400.0
                  },
                  "regular_hours": {
                    "type": "number",
                    "format": "float",
                    "example": 160.0
                  },
                  "overtime_hours": {
                    "type": "number",
                    "format": "float",
                    "example": 10.0
                  },
                  "total_hours": {
                    "type": "number",
                    "format": "float",
                    "example": 170.0
                  },
                  "federal_tax": {
                    "type": "number",
                    "format": "float",
                    "example": 600.0
                  },
                  "state_tax": {
                    "type": "number",
                    "format": "float",
                    "example": 200.0
                  },
                  "social_security": {
                    "type": "number",
                    "format": "float",
                    "example": 271.25
                  },
                  "medicare": {
                    "type": "number",
                    "format": "float",
                    "example": 63.44
                  }
                }
              },
              "connection": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "integer",
                    "example": 1
                  },
                  "name": {
                    "type": "string",
                    "example": "ADP Payroll Connection"
                  },
                  "provider": {
                    "type": "string",
                    "example": "adp"
                  }
                }
              }
            }
          }
        ]
      },
      "PayPeriod": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "2024-01-15_2024-01-21"
          },
          "start_date": {
            "type": "string",
            "format": "date",
            "example": "2024-01-15"
          },
          "end_date": {
            "type": "string",
            "format": "date",
            "example": "2024-01-21"
          },
          "period_type": {
            "type": "string",
            "enum": [
              "weekly",
              "bi-weekly",
              "semi-monthly",
              "monthly"
            ],
            "example": "weekly"
          },
          "days_in_period": {
            "type": "integer",
            "example": 7
          },
          "is_current": {
            "type": "boolean",
            "example": true
          },
          "is_future": {
            "type": "boolean",
            "example": false
          },
          "is_past": {
            "type": "boolean",
            "example": false
          }
        }
      },
      "PayPeriodSummary": {
        "type": "object",
        "properties": {
          "period": {
            "$ref": "#/components/schemas/PayPeriod"
          },
          "hours": {
            "type": "object",
            "properties": {
              "timesheet": {
                "type": "object",
                "properties": {
                  "regular": {
                    "type": "number",
                    "format": "float",
                    "example": 40.0
                  },
                  "overtime": {
                    "type": "number",
                    "format": "float",
                    "example": 2.5
                  },
                  "total": {
                    "type": "number",
                    "format": "float",
                    "example": 42.5
                  }
                }
              },
              "attendance": {
                "type": "object",
                "properties": {
                  "regular": {
                    "type": "number",
                    "format": "float",
                    "example": 39.5
                  },
                  "overtime": {
                    "type": "number",
                    "format": "float",
                    "example": 2.0
                  },
                  "total": {
                    "type": "number",
                    "format": "float",
                    "example": 41.5
                  }
                }
              },
              "scheduled": {
                "type": "number",
                "format": "float",
                "example": 40.0
              },
              "variance": {
                "type": "object",
                "properties": {
                  "timesheet_vs_scheduled": {
                    "type": "number",
                    "format": "float",
                    "example": 2.5
                  },
                  "attendance_vs_scheduled": {
                    "type": "number",
                    "format": "float",
                    "example": 1.5
                  },
                  "timesheet_vs_attendance": {
                    "type": "number",
                    "format": "float",
                    "example": 1.0
                  }
                }
              }
            }
          },
          "attendance": {
            "type": "object",
            "properties": {
              "total_records": {
                "type": "integer",
                "example": 5
              },
              "completed_shifts": {
                "type": "integer",
                "example": 5
              },
              "missed_shifts": {
                "type": "integer",
                "example": 0
              },
              "late_arrivals": {
                "type": "integer",
                "example": 1
              },
              "early_departures": {
                "type": "integer",
                "example": 0
              }
            }
          },
          "estimated_pay": {
            "type": "number",
            "format": "float",
            "example": 1093.75
          },
          "timesheet_status": {
            "type": "string",
            "enum": [
              "not_created",
              "pending",
              "submitted",
              "approved",
              "rejected"
            ],
            "example": "pending"
          },
          "completion_percentage": {
            "type": "number",
            "format": "float",
            "example": 85.7
          }
        }
      },
      "ManualTimeEntry": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 456
          },
          "timesheet_id": {
            "type": "integer",
            "example": 123
          },
          "date": {
            "type": "string",
            "format": "date",
            "example": "2024-01-15"
          },
          "start_time": {
            "type": "string",
            "format": "time",
            "example": "09:00:00"
          },
          "end_time": {
            "type": "string",
            "format": "time",
            "example": "17:00:00"
          },
          "hours": {
            "type": "number",
            "format": "float",
            "example": 8.0
          },
          "entry_type": {
            "type": "string",
            "enum": [
              "missing_punch",
              "regular",
              "overtime"
            ],
            "example": "missing_punch"
          },
          "edit_notes": {
            "type": "string",
            "example": "Manual entry for missed punch"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "approved",
              "rejected"
            ],
            "example": "pending"
          },
          "shift": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "example": 789
              },
              "title": {
                "type": "string",
                "example": "Morning Shift"
              },
              "location": {
                "type": "string",
                "example": "Main Office"
              }
            }
          },
          "timesheet": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "example": 123
              },
              "start_date": {
                "type": "string",
                "format": "date",
                "example": "2024-01-15"
              },
              "end_date": {
                "type": "string",
                "format": "date",
                "example": "2024-01-21"
              },
              "status": {
                "type": "string",
                "enum": [
                  "pending",
                  "submitted",
                  "approved",
                  "rejected"
                ],
                "example": "pending"
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2024-01-15T18:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2024-01-15T18:00:00Z"
          }
        }
      },
      "ManualTimeEntryDetailed": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ManualTimeEntry"
          },
          {
            "type": "object",
            "properties": {
              "edit_history": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer",
                      "example": 1
                    },
                    "field_changed": {
                      "type": "string",
                      "example": "end_time"
                    },
                    "original_value": {
                      "type": "string",
                      "example": "16:30:00"
                    },
                    "new_value": {
                      "type": "string",
                      "example": "17:00:00"
                    },
                    "reason": {
                      "type": "string",
                      "example": "Employee requested time correction"
                    },
                    "edited_by": {
                      "type": "string",
                      "example": "John Doe"
                    },
                    "edited_at": {
                      "type": "string",
                      "format": "date-time",
                      "example": "2024-01-16T09:00:00Z"
                    }
                  }
                }
              },
              "validation_warnings": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "type": {
                      "type": "string",
                      "enum": [
                        "overlap",
                        "excessive_hours",
                        "weekend_work"
                      ],
                      "example": "overlap"
                    },
                    "message": {
                      "type": "string",
                      "example": "This entry overlaps with another time entry on the same date"
                    }
                  }
                }
              },
              "pay_calculation": {
                "type": "object",
                "properties": {
                  "regular_hours": {
                    "type": "number",
                    "format": "float",
                    "example": 8.0
                  },
                  "overtime_hours": {
                    "type": "number",
                    "format": "float",
                    "example": 0.0
                  },
                  "regular_pay": {
                    "type": "number",
                    "format": "float",
                    "example": 200.0
                  },
                  "overtime_pay": {
                    "type": "number",
                    "format": "float",
                    "example": 0.0
                  },
                  "total_pay": {
                    "type": "number",
                    "format": "float",
                    "example": 200.0
                  },
                  "hourly_rate": {
                    "type": "number",
                    "format": "float",
                    "example": 25.0
                  },
                  "overtime_rate": {
                    "type": "number",
                    "format": "float",
                    "example": 37.5
                  }
                }
              }
            }
          }
        ]
      },
      "Skill": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 123
          },
          "name": {
            "type": "string",
            "example": "JavaScript Development"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "example": "Frontend and backend JavaScript programming"
          },
          "category": {
            "type": "string",
            "nullable": true,
            "example": "technical_skills"
          },
          "category_display": {
            "type": "string",
            "nullable": true,
            "example": "Technical Skills"
          },
          "requires_certification": {
            "type": "boolean",
            "example": true
          },
          "skill_kind": {
            "type": "string",
            "enum": [
              "skill",
              "certification",
              "license"
            ],
            "description": "Catalog credential type. Clients map `license` -> LIC badge, `certification` -> CERT badge, `skill` -> no badge.\n",
            "example": "certification"
          },
          "requires_document_upload": {
            "type": "boolean",
            "example": false
          },
          "active": {
            "type": "boolean",
            "example": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2024-01-15T10:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2024-01-15T10:00:00Z"
          }
        }
      },
      "SkillDetailed": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Skill"
          },
          {
            "type": "object",
            "properties": {
              "metadata": {
                "type": "object",
                "nullable": true,
                "description": "Additional skill metadata"
              },
              "help_desk_tier": {
                "type": "integer",
                "nullable": true,
                "example": 2
              },
              "employee_count": {
                "type": "integer",
                "example": 15
              },
              "average_proficiency": {
                "type": "number",
                "format": "float",
                "nullable": true,
                "example": 3.4
              }
            }
          }
        ]
      },
      "EmployeeSkill": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 456
          },
          "skill_id": {
            "type": "integer",
            "example": 123
          },
          "proficiency_level": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5,
            "example": 3
          },
          "certification_date": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "example": "2024-01-15"
          },
          "expiration_date": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "example": "2025-01-15"
          },
          "certification_number": {
            "type": "string",
            "nullable": true,
            "example": "CERT-2024-001"
          },
          "notes": {
            "type": "string",
            "nullable": true,
            "example": "Completed advanced course"
          },
          "verified": {
            "type": "boolean",
            "example": true
          },
          "verified_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": "2024-01-16T10:00:00Z"
          },
          "active": {
            "type": "boolean",
            "example": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2024-01-15T10:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2024-01-15T10:00:00Z"
          },
          "skill": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "example": 123
              },
              "name": {
                "type": "string",
                "example": "JavaScript Development"
              },
              "category": {
                "type": "string",
                "nullable": true,
                "example": "technical_skills"
              },
              "category_display": {
                "type": "string",
                "nullable": true,
                "example": "Technical Skills"
              },
              "requires_certification": {
                "type": "boolean",
                "example": true
              },
              "skill_kind": {
                "type": "string",
                "enum": [
                  "skill",
                  "certification",
                  "license"
                ],
                "description": "Raw catalog credential type. Drives the LIC/CERT badge in the web/mobile UI — clients map license -> \"LIC\", certification -> \"CERT\", skill -> no badge.",
                "example": "certification"
              }
            }
          }
        }
      },
      "EmployeeSkillDetailed": {
        "allOf": [
          {
            "$ref": "#/components/schemas/EmployeeSkill"
          },
          {
            "type": "object",
            "properties": {
              "verifier": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "integer",
                    "example": 789
                  },
                  "name": {
                    "type": "string",
                    "example": "John Manager"
                  }
                }
              },
              "days_until_expiration": {
                "type": "integer",
                "nullable": true,
                "example": 45
              },
              "expired": {
                "type": "boolean",
                "example": false
              },
              "expiring_soon": {
                "type": "boolean",
                "example": true
              },
              "proficiency_text": {
                "type": "string",
                "example": "Intermediate"
              },
              "verification_status": {
                "type": "string",
                "enum": [
                  "verified",
                  "pending",
                  "unverified"
                ],
                "example": "verified"
              }
            }
          }
        ]
      },
      "Certification": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 456
          },
          "skill_id": {
            "type": "integer",
            "example": 123
          },
          "certification_date": {
            "type": "string",
            "format": "date",
            "example": "2024-01-15"
          },
          "expiration_date": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "example": "2025-01-15"
          },
          "certification_number": {
            "type": "string",
            "nullable": true,
            "example": "CERT-2024-001"
          },
          "verified": {
            "type": "boolean",
            "example": true
          },
          "verified_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": "2024-01-16T10:00:00Z"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2024-01-15T10:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2024-01-15T10:00:00Z"
          },
          "skill": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "example": 123
              },
              "name": {
                "type": "string",
                "example": "JavaScript Development"
              },
              "category": {
                "type": "string",
                "nullable": true,
                "example": "technical_skills"
              },
              "category_display": {
                "type": "string",
                "nullable": true,
                "example": "Technical Skills"
              }
            }
          }
        }
      },
      "CertificationDetailed": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Certification"
          },
          {
            "type": "object",
            "properties": {
              "notes": {
                "type": "string",
                "nullable": true,
                "example": "Completed advanced course"
              },
              "proficiency_level": {
                "type": "integer",
                "minimum": 1,
                "maximum": 5,
                "example": 3
              },
              "verifier": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "integer",
                    "example": 789
                  },
                  "name": {
                    "type": "string",
                    "example": "John Manager"
                  }
                }
              },
              "days_until_expiration": {
                "type": "integer",
                "nullable": true,
                "example": 45
              },
              "expired": {
                "type": "boolean",
                "example": false
              },
              "expiring_soon": {
                "type": "boolean",
                "example": true
              },
              "status": {
                "type": "string",
                "enum": [
                  "active",
                  "expiring_soon",
                  "expiring_notice",
                  "expired"
                ],
                "example": "active"
              },
              "document_attached": {
                "type": "boolean",
                "example": true
              }
            }
          }
        ]
      },
      "LoginConfiguration": {
        "type": "object",
        "properties": {
          "business": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "example": 123
              },
              "name": {
                "type": "string",
                "example": "Office Chat Solutions"
              },
              "subdomain": {
                "type": "string",
                "example": "officechat"
              },
              "logo_url": {
                "type": "string",
                "nullable": true,
                "example": "https://cdn.workforce.mangoapps.com/logos/officechat.png"
              },
              "timezone": {
                "type": "string",
                "example": "America/New_York"
              },
              "branding": {
                "$ref": "#/components/schemas/Business/properties/branding"
              }
            }
          },
          "login_methods": {
            "type": "object",
            "properties": {
              "email_password": {
                "type": "object",
                "properties": {
                  "enabled": {
                    "type": "boolean",
                    "example": true
                  },
                  "forgot_password_enabled": {
                    "type": "boolean",
                    "example": true
                  }
                }
              },
              "sso_providers": {
                "type": "array",
                "description": "Array of configured SSO providers for this business. Multiple providers of the same\ntype (e.g., multiple Google OAuth2 configurations) are supported. Each provider has\na unique ID that should be passed to the SSO initiate endpoint.\n",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "Unique identifier for this SSO configuration (use as provider_id)",
                      "example": "67"
                    },
                    "provider_type": {
                      "type": "string",
                      "enum": [
                        "google_oauth2",
                        "saml",
                        "entra_id",
                        "mangoapps"
                      ],
                      "description": "The underlying technology/provider type",
                      "example": "google_oauth2"
                    },
                    "provider_name": {
                      "type": "string",
                      "description": "User-friendly name for this SSO provider",
                      "example": "Google Workspace - Partners"
                    },
                    "enabled": {
                      "type": "boolean",
                      "description": "Whether this SSO configuration is currently active",
                      "example": true
                    },
                    "logo_url": {
                      "type": "string",
                      "nullable": true,
                      "description": "URL to the provider's logo for display in UI",
                      "example": "https://cdn.workforce.mangoapps.com/logos/google.png"
                    },
                    "description": {
                      "type": "string",
                      "description": "A short description for the SSO provider",
                      "example": "Sign in with your Google Workspace account"
                    },
                    "channel_support": {
                      "type": "string",
                      "enum": [
                        "web_only",
                        "mobile_only",
                        "both"
                      ],
                      "description": "Indicates which channels (web, mobile, or both) this SSO configuration supports",
                      "example": "both"
                    },
                    "mobile_supported": {
                      "type": "boolean",
                      "description": "Derived flag indicating if this SSO configuration is available for mobile apps",
                      "example": true
                    },
                    "web_supported": {
                      "type": "boolean",
                      "description": "Derived flag indicating if this SSO configuration is available for web browsers",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "two_factor_authentication": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "example": true
              },
              "methods": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "sms",
                    "email",
                    "authenticator_app",
                    "backup_codes"
                  ]
                },
                "example": [
                  "sms",
                  "authenticator_app"
                ]
              },
              "backup_codes": {
                "type": "object",
                "properties": {
                  "enabled": {
                    "type": "boolean",
                    "example": true
                  },
                  "count": {
                    "type": "integer",
                    "example": 8
                  }
                }
              }
            }
          },
          "passwordless_authentication": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "example": true
              },
              "methods": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "magic_link",
                    "sms_code",
                    "email_code"
                  ]
                },
                "example": [
                  "magic_link",
                  "email_code"
                ]
              }
            }
          },
          "biometric_authentication": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "example": true
              },
              "supported_methods": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "face_id",
                    "touch_id",
                    "fingerprint"
                  ]
                },
                "example": [
                  "face_id",
                  "touch_id",
                  "fingerprint"
                ]
              }
            }
          },
          "remember_device": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "example": true
              },
              "duration_days": {
                "type": "integer",
                "example": 30
              }
            }
          },
          "api_endpoints": {
            "type": "object",
            "properties": {
              "login": {
                "type": "string",
                "example": "/api/v1/auth/login"
              },
              "sso_initiate": {
                "type": "string",
                "example": "/api/v1/auth/sso/initiate"
              },
              "sso_exchange": {
                "type": "string",
                "example": "/api/v1/auth/sso/exchange"
              },
              "refresh_token": {
                "type": "string",
                "example": "/api/v1/auth/refresh"
              },
              "logout": {
                "type": "string",
                "example": "/api/v1/auth/logout"
              },
              "passwordless_request": {
                "type": "string",
                "example": "/api/v1/auth/passwordless/request"
              },
              "passwordless_verify": {
                "type": "string",
                "example": "/api/v1/auth/passwordless/verify"
              }
            }
          }
        }
      },
      "SsoInitiateResponse": {
        "type": "object",
        "properties": {
          "authorization_url": {
            "type": "string",
            "description": "URL to open for SSO authentication",
            "example": "https://accounts.google.com/o/oauth2/v2/auth?client_id=..."
          },
          "state": {
            "type": "string",
            "description": "State parameter for security verification",
            "example": "abc123def456"
          },
          "app_redirect_uri": {
            "type": "string",
            "nullable": true,
            "description": "Echo of the accepted `app_redirect_uri`, or null when the submitted value was\ndropped by the allowlist. Desktop clients MUST compare this against what they\nsent: a value that does not come back will never be reached by the callback\npage, so fall back to a custom scheme BEFORE opening the browser rather than\nwaiting out a sign-in that cannot complete. Servers predating this field omit\nit entirely, which clients must also read as \"not accepted\".\n",
            "example": "http://127.0.0.1:52341/sso-callback"
          },
          "provider": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "example": "google_oauth2"
              },
              "name": {
                "type": "string",
                "example": "Google Workspace"
              }
            }
          },
          "expires_in": {
            "type": "integer",
            "description": "State expiration time in seconds",
            "example": 600
          },
          "instructions": {
            "type": "object",
            "properties": {
              "message": {
                "type": "string",
                "example": "Open the authorization URL in a web view or external browser"
              },
              "next_step": {
                "type": "string",
                "example": "After authorization, exchange the received code using the /exchange endpoint"
              }
            }
          }
        }
      },
      "SsoExchangeResponse": {
        "type": "object",
        "properties": {
          "access_token": {
            "type": "string",
            "description": "Access token for API requests",
            "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
          },
          "refresh_token": {
            "type": "string",
            "description": "Refresh token for token renewal",
            "example": "def456ghi789..."
          },
          "expires_in": {
            "type": "integer",
            "description": "Access token expiration time in seconds",
            "example": 3600
          },
          "token_type": {
            "type": "string",
            "example": "Bearer"
          },
          "user": {
            "$ref": "#/components/schemas/User"
          },
          "business": {
            "$ref": "#/components/schemas/Business"
          },
          "sso_provider": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "example": "google_oauth2"
              },
              "name": {
                "type": "string",
                "example": "Google Workspace"
              }
            }
          }
        }
      },
      "EnhancedUser": {
        "type": "object",
        "description": "Enhanced user profile with additional security and metadata",
        "properties": {
          "id": {
            "type": "integer",
            "example": 123
          },
          "email": {
            "type": "string",
            "format": "email",
            "example": "john.doe@example.com"
          },
          "first_name": {
            "type": "string",
            "example": "John"
          },
          "last_name": {
            "type": "string",
            "example": "Doe"
          },
          "full_name": {
            "type": "string",
            "example": "John Doe"
          },
          "role": {
            "type": "string",
            "example": "Employee"
          },
          "active": {
            "type": "boolean",
            "example": true
          },
          "avatar_url": {
            "type": "string",
            "nullable": true,
            "example": "https://example.com/avatar.jpg"
          },
          "phone": {
            "type": "string",
            "nullable": true,
            "example": "+1234567890"
          },
          "time_zone": {
            "type": "string",
            "nullable": true,
            "example": "America/New_York"
          },
          "employee_type": {
            "type": "string",
            "nullable": true,
            "example": "Full-Time"
          },
          "hire_date": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "example": "2023-01-15"
          },
          "last_activity_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": "2024-01-15T10:30:00Z"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2023-01-15T09:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2024-01-15T10:30:00Z"
          }
        }
      },
      "SecurityInfo": {
        "type": "object",
        "description": "User security information and status",
        "properties": {
          "two_factor_enabled": {
            "type": "boolean",
            "description": "Whether 2FA is enabled for the user",
            "example": true
          },
          "two_factor_status": {
            "type": "string",
            "enum": [
              "disabled",
              "pending_setup",
              "pending_confirmation",
              "enabled"
            ],
            "description": "Current 2FA setup status",
            "example": "enabled"
          },
          "account_locked": {
            "type": "boolean",
            "description": "Whether the account is currently locked",
            "example": false
          },
          "last_sign_in_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Last successful sign-in timestamp",
            "example": "2024-01-15T08:30:00Z"
          },
          "last_sign_in_ip": {
            "type": "string",
            "nullable": true,
            "description": "IP address of last sign-in",
            "example": "192.168.1.100"
          },
          "sign_in_count": {
            "type": "integer",
            "description": "Total number of sign-ins",
            "example": 42
          },
          "failed_attempts": {
            "type": "integer",
            "description": "Number of failed login attempts",
            "example": 0
          },
          "locked_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the account was locked"
          },
          "password_changed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the password was last changed",
            "example": "2024-01-01T00:00:00Z"
          }
        }
      },
      "Session": {
        "type": "object",
        "description": "User session information",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Session ID",
            "example": 456
          },
          "name": {
            "type": "string",
            "description": "Session name/description",
            "example": "Mobile Access Token - 2024-01-15 10:30"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the session was created",
            "example": "2024-01-15T10:30:00Z"
          },
          "last_used_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the session was last used",
            "example": "2024-01-15T14:30:00Z"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the session expires",
            "example": "2024-01-15T11:30:00Z"
          },
          "is_current": {
            "type": "boolean",
            "description": "Whether this is the current session",
            "example": true
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "idle",
              "expired",
              "revoked"
            ],
            "description": "Current session status",
            "example": "active"
          }
        }
      },
      "DetailedSession": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Session"
          },
          {
            "type": "object",
            "properties": {
              "scopes": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Permission scopes for this session",
                "example": [
                  "read:shifts",
                  "write:shifts",
                  "read:users"
                ]
              },
              "business_id": {
                "type": "integer",
                "description": "Associated business ID",
                "example": 789
              },
              "user_agent": {
                "type": "object",
                "description": "User agent information",
                "properties": {
                  "browser": {
                    "type": "string",
                    "example": "Chrome"
                  },
                  "platform": {
                    "type": "string",
                    "example": "macOS"
                  },
                  "raw": {
                    "type": "string",
                    "example": "Mobile Access Token - 2024-01-15 10:30"
                  }
                }
              },
              "location_info": {
                "type": "object",
                "description": "Location information (if available)",
                "properties": {
                  "ip_address": {
                    "type": "string",
                    "nullable": true,
                    "description": "IP address (not exposed for security)"
                  },
                  "country": {
                    "type": "string",
                    "nullable": true,
                    "example": "United States"
                  },
                  "city": {
                    "type": "string",
                    "nullable": true,
                    "example": "New York"
                  },
                  "estimated": {
                    "type": "boolean",
                    "description": "Whether location is estimated",
                    "example": true
                  }
                }
              },
              "device_info": {
                "type": "object",
                "description": "Device information",
                "properties": {
                  "type": {
                    "type": "string",
                    "enum": [
                      "mobile",
                      "tablet",
                      "desktop"
                    ],
                    "example": "mobile"
                  },
                  "name": {
                    "type": "string",
                    "example": "Mobile Access Token - 2024-01-15 10:30"
                  },
                  "trusted": {
                    "type": "boolean",
                    "description": "Whether the device is considered trusted",
                    "example": true
                  }
                }
              },
              "security_info": {
                "type": "object",
                "description": "Security-related session information",
                "properties": {
                  "service_account": {
                    "type": "boolean",
                    "description": "Whether this is a service account token",
                    "example": false
                  },
                  "has_secret": {
                    "type": "boolean",
                    "description": "Whether the token has an associated secret",
                    "example": false
                  },
                  "token_type": {
                    "type": "string",
                    "enum": [
                      "user_session",
                      "service_account"
                    ],
                    "description": "Type of authentication token",
                    "example": "user_session"
                  }
                }
              }
            }
          }
        ]
      },
      "PasswordlessToken": {
        "type": "object",
        "description": "Passwordless authentication token information",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Token ID",
            "example": 123
          },
          "type": {
            "type": "string",
            "enum": [
              "magic_link",
              "verification_code",
              "sms_code"
            ],
            "description": "Type of passwordless token",
            "example": "magic_link"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the token was created",
            "example": "2024-01-15T10:30:00Z"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the token expires",
            "example": "2024-01-15T11:30:00Z"
          },
          "verified_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the token was verified",
            "example": "2024-01-15T10:35:00Z"
          },
          "expired": {
            "type": "boolean",
            "description": "Whether the token has expired",
            "example": false
          },
          "verified": {
            "type": "boolean",
            "description": "Whether the token has been verified",
            "example": true
          },
          "revoked": {
            "type": "boolean",
            "description": "Whether the token has been revoked",
            "example": false
          }
        }
      },
      "PersonalApiToken": {
        "type": "object",
        "description": "Personal API token information",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Token ID",
            "example": 456
          },
          "name": {
            "type": "string",
            "description": "Token name/description",
            "example": "My Automation Token"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the token was created",
            "example": "2024-01-15T10:30:00Z"
          },
          "last_used_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the token was last used",
            "example": "2024-01-15T14:30:00Z"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the token expires",
            "example": "2024-12-31T23:59:59Z"
          },
          "expired": {
            "type": "boolean",
            "description": "Whether the token has expired",
            "example": false
          },
          "active": {
            "type": "boolean",
            "description": "Whether the token is active",
            "example": true
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Token scopes/permissions",
            "example": [
              "read:own_profile",
              "read:own_shifts"
            ]
          }
        }
      },
      "DetailedPersonalApiToken": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PersonalApiToken"
          },
          {
            "type": "object",
            "properties": {
              "token": {
                "type": "string",
                "description": "The actual token value (only shown on creation/rotation)",
                "example": "pat_1234567890abcdef"
              },
              "business_id": {
                "type": "integer",
                "description": "Associated business ID",
                "example": 789
              },
              "service_account": {
                "type": "boolean",
                "description": "Whether this is a service account token",
                "example": false
              },
              "scope_count": {
                "type": "integer",
                "description": "Number of scopes assigned",
                "example": 4
              },
              "days_until_expiration": {
                "type": "integer",
                "nullable": true,
                "description": "Days until expiration (null if no expiration)",
                "example": 365
              },
              "usage_summary": {
                "type": "object",
                "description": "Usage summary information",
                "properties": {
                  "last_used": {
                    "type": "string",
                    "description": "Human-readable last used time",
                    "example": "2 hours ago"
                  },
                  "created": {
                    "type": "string",
                    "description": "Human-readable creation time",
                    "example": "3 days ago"
                  }
                }
              }
            }
          }
        ]
      },
      "TokenUsage": {
        "type": "object",
        "description": "Token usage statistics and analytics",
        "properties": {
          "token_id": {
            "type": "integer",
            "description": "Token ID",
            "example": 456
          },
          "token_name": {
            "type": "string",
            "description": "Token name",
            "example": "My Automation Token"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Token creation date",
            "example": "2024-01-15T10:30:00Z"
          },
          "last_used_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Last usage timestamp",
            "example": "2024-01-15T14:30:00Z"
          },
          "total_requests": {
            "type": "integer",
            "description": "Total number of API requests",
            "example": 1250
          },
          "requests_last_24h": {
            "type": "integer",
            "description": "Requests in the last 24 hours",
            "example": 45
          },
          "requests_last_7d": {
            "type": "integer",
            "description": "Requests in the last 7 days",
            "example": 320
          },
          "requests_last_30d": {
            "type": "integer",
            "description": "Requests in the last 30 days",
            "example": 1100
          },
          "most_used_endpoints": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "example": "/api/v1/shifts"
                },
                "method": {
                  "type": "string",
                  "example": "GET"
                },
                "count": {
                  "type": "integer",
                  "example": 150
                }
              }
            },
            "description": "Most frequently used endpoints"
          },
          "recent_activity": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "timestamp": {
                  "type": "string",
                  "format": "date-time",
                  "example": "2024-01-15T14:30:00Z"
                },
                "endpoint": {
                  "type": "string",
                  "example": "/api/v1/auth/profile"
                },
                "method": {
                  "type": "string",
                  "example": "GET"
                },
                "status_code": {
                  "type": "integer",
                  "example": 200
                },
                "ip_address": {
                  "type": "string",
                  "example": "192.168.1.100"
                }
              }
            },
            "description": "Recent API activity"
          }
        }
      },
      "UserSecuritySettings": {
        "type": "object",
        "description": "User security settings and status",
        "properties": {
          "two_factor_auth": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Whether two-factor authentication is enabled",
                "example": true
              },
              "backup_codes_count": {
                "type": "integer",
                "description": "Number of remaining backup codes",
                "example": 8
              },
              "last_used": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When 2FA was last used",
                "example": "2024-01-15T10:30:00Z"
              },
              "setup_required": {
                "type": "boolean",
                "description": "Whether 2FA setup is required",
                "example": false
              }
            }
          },
          "trusted_devices": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Device ID",
                  "example": "device_123"
                },
                "name": {
                  "type": "string",
                  "description": "Device name",
                  "example": "iPhone 15 Pro"
                },
                "last_seen": {
                  "type": "string",
                  "format": "date-time",
                  "description": "When device was last seen",
                  "example": "2024-01-15T14:22:00Z"
                },
                "location": {
                  "type": "string",
                  "nullable": true,
                  "description": "Device location",
                  "example": "New York, NY"
                },
                "ip_address": {
                  "type": "string",
                  "description": "Device IP address",
                  "example": "192.168.1.100"
                },
                "is_current": {
                  "type": "boolean",
                  "description": "Whether this is the current device",
                  "example": true
                }
              }
            }
          },
          "security_questions": {
            "type": "object",
            "properties": {
              "configured": {
                "type": "boolean",
                "description": "Whether security questions are configured",
                "example": true
              },
              "last_updated": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When security questions were last updated",
                "example": "2024-01-10T09:15:00Z"
              }
            }
          },
          "login_history": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "timestamp": {
                  "type": "string",
                  "format": "date-time",
                  "description": "Login attempt timestamp",
                  "example": "2024-01-15T14:22:00Z"
                },
                "ip_address": {
                  "type": "string",
                  "description": "IP address of login attempt",
                  "example": "192.168.1.100"
                },
                "location": {
                  "type": "string",
                  "nullable": true,
                  "description": "Geographic location of login",
                  "example": "New York, NY"
                },
                "device": {
                  "type": "string",
                  "description": "Device information",
                  "example": "iPhone 15 Pro"
                },
                "success": {
                  "type": "boolean",
                  "description": "Whether login was successful",
                  "example": true
                },
                "failure_reason": {
                  "type": "string",
                  "nullable": true,
                  "description": "Reason for login failure",
                  "example": null
                }
              }
            }
          },
          "password_security": {
            "type": "object",
            "properties": {
              "last_changed": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When password was last changed",
                "example": "2024-01-01T00:00:00Z"
              },
              "strength_score": {
                "type": "integer",
                "description": "Password strength score (0-100)",
                "example": 85
              },
              "expires_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When password expires",
                "example": null
              }
            }
          }
        }
      },
      "AccountSettingsDashboard": {
        "type": "object",
        "description": "Account settings dashboard overview",
        "properties": {
          "profile_completion": {
            "type": "object",
            "properties": {
              "percentage": {
                "type": "integer",
                "description": "Profile completion percentage",
                "example": 85
              },
              "missing_fields": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "List of missing profile fields",
                "example": [
                  "emergency_contact",
                  "skills"
                ]
              },
              "next_steps": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "action": {
                      "type": "string",
                      "description": "Action identifier",
                      "example": "complete_emergency_contact"
                    },
                    "title": {
                      "type": "string",
                      "description": "Action title",
                      "example": "Add Emergency Contact"
                    },
                    "description": {
                      "type": "string",
                      "description": "Action description",
                      "example": "Add an emergency contact for safety"
                    },
                    "priority": {
                      "type": "string",
                      "enum": [
                        "high",
                        "medium",
                        "low"
                      ],
                      "description": "Action priority",
                      "example": "high"
                    }
                  }
                }
              }
            }
          },
          "security_status": {
            "type": "object",
            "properties": {
              "score": {
                "type": "integer",
                "description": "Security score (0-100)",
                "example": 90
              },
              "two_factor_enabled": {
                "type": "boolean",
                "description": "Whether 2FA is enabled",
                "example": true
              },
              "password_strength": {
                "type": "string",
                "enum": [
                  "weak",
                  "medium",
                  "strong"
                ],
                "description": "Password strength assessment",
                "example": "strong"
              },
              "recommendations": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Security recommendations",
                "example": [
                  "Review trusted devices"
                ]
              }
            }
          },
          "notification_summary": {
            "type": "object",
            "properties": {
              "total_categories": {
                "type": "integer",
                "description": "Total notification categories",
                "example": 12
              },
              "enabled_categories": {
                "type": "integer",
                "description": "Number of enabled categories",
                "example": 8
              },
              "unread_count": {
                "type": "integer",
                "description": "Number of unread notifications",
                "example": 3
              },
              "last_notification": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "Timestamp of last notification",
                "example": "2024-01-15T10:30:00Z"
              }
            }
          },
          "recent_activity": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "action": {
                  "type": "string",
                  "description": "Activity action",
                  "example": "profile_updated"
                },
                "timestamp": {
                  "type": "string",
                  "format": "date-time",
                  "description": "Activity timestamp",
                  "example": "2024-01-15T10:30:00Z"
                },
                "description": {
                  "type": "string",
                  "description": "Activity description",
                  "example": "Updated phone number"
                },
                "ip_address": {
                  "type": "string",
                  "nullable": true,
                  "description": "IP address of activity",
                  "example": "192.168.1.100"
                }
              }
            }
          },
          "quick_actions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Action identifier",
                  "example": "upload_profile_photo"
                },
                "title": {
                  "type": "string",
                  "description": "Action title",
                  "example": "Upload Profile Photo"
                },
                "description": {
                  "type": "string",
                  "description": "Action description",
                  "example": "Personalize your account"
                },
                "icon": {
                  "type": "string",
                  "description": "Action icon",
                  "example": "camera"
                },
                "priority": {
                  "type": "string",
                  "enum": [
                    "high",
                    "medium",
                    "low"
                  ],
                  "description": "Action priority",
                  "example": "medium"
                }
              }
            }
          }
        }
      },
      "UserPrivacySettings": {
        "type": "object",
        "description": "User privacy and data management settings",
        "properties": {
          "data_sharing": {
            "type": "object",
            "properties": {
              "analytics_enabled": {
                "type": "boolean",
                "description": "Whether analytics data sharing is enabled",
                "example": true
              },
              "performance_tracking": {
                "type": "boolean",
                "description": "Whether performance tracking is enabled",
                "example": true
              },
              "location_sharing": {
                "type": "boolean",
                "description": "Whether location sharing is enabled",
                "example": false
              }
            }
          },
          "visibility_settings": {
            "type": "object",
            "properties": {
              "profile_visibility": {
                "type": "string",
                "enum": [
                  "public",
                  "team_only",
                  "managers_only",
                  "private"
                ],
                "description": "Profile visibility level",
                "example": "team_only"
              },
              "contact_info_visibility": {
                "type": "string",
                "enum": [
                  "public",
                  "team_only",
                  "managers_only",
                  "private"
                ],
                "description": "Contact information visibility",
                "example": "managers_only"
              },
              "schedule_visibility": {
                "type": "string",
                "enum": [
                  "public",
                  "team_only",
                  "managers_only",
                  "private"
                ],
                "description": "Schedule visibility level",
                "example": "public"
              }
            }
          },
          "data_retention": {
            "type": "object",
            "properties": {
              "auto_delete_old_data": {
                "type": "boolean",
                "description": "Whether to automatically delete old data",
                "example": false
              },
              "retention_period_months": {
                "type": "integer",
                "description": "Data retention period in months",
                "example": 24
              }
            }
          },
          "export_options": {
            "type": "object",
            "properties": {
              "last_export": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When data was last exported",
                "example": "2024-01-01T00:00:00Z"
              },
              "available_formats": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Available export formats",
                "example": [
                  "json",
                  "csv",
                  "pdf"
                ]
              }
            }
          }
        }
      },
      "UserPrivacySettingsUpdate": {
        "type": "object",
        "description": "Privacy settings update payload",
        "properties": {
          "data_sharing": {
            "type": "object",
            "properties": {
              "analytics_enabled": {
                "type": "boolean"
              },
              "performance_tracking": {
                "type": "boolean"
              },
              "location_sharing": {
                "type": "boolean"
              }
            }
          },
          "visibility_settings": {
            "type": "object",
            "properties": {
              "profile_visibility": {
                "type": "string",
                "enum": [
                  "public",
                  "team_only",
                  "managers_only",
                  "private"
                ]
              },
              "contact_info_visibility": {
                "type": "string",
                "enum": [
                  "public",
                  "team_only",
                  "managers_only",
                  "private"
                ]
              },
              "schedule_visibility": {
                "type": "string",
                "enum": [
                  "public",
                  "team_only",
                  "managers_only",
                  "private"
                ]
              }
            }
          },
          "data_retention": {
            "type": "object",
            "properties": {
              "auto_delete_old_data": {
                "type": "boolean"
              },
              "retention_period_months": {
                "type": "integer"
              }
            }
          }
        }
      },
      "UserCommunicationSettings": {
        "type": "object",
        "description": "User communication preferences",
        "properties": {
          "preferred_language": {
            "type": "string",
            "description": "User's preferred language",
            "example": "en"
          },
          "timezone": {
            "type": "string",
            "description": "User's timezone",
            "example": "America/New_York"
          },
          "communication_style": {
            "type": "object",
            "properties": {
              "formality": {
                "type": "string",
                "enum": [
                  "casual",
                  "professional",
                  "formal"
                ],
                "description": "Communication formality level",
                "example": "professional"
              },
              "frequency": {
                "type": "string",
                "enum": [
                  "minimal",
                  "normal",
                  "frequent"
                ],
                "description": "Communication frequency preference",
                "example": "normal"
              },
              "channel_preference": {
                "type": "string",
                "enum": [
                  "email",
                  "sms",
                  "in_app",
                  "phone"
                ],
                "description": "Preferred communication channel",
                "example": "email"
              }
            }
          },
          "ai_assistant": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Whether AI assistant is enabled",
                "example": true
              },
              "personality": {
                "type": "string",
                "enum": [
                  "helpful",
                  "concise",
                  "detailed",
                  "friendly"
                ],
                "description": "AI assistant personality",
                "example": "helpful"
              },
              "proactivity_level": {
                "type": "string",
                "enum": [
                  "low",
                  "medium",
                  "high"
                ],
                "description": "AI assistant proactivity level",
                "example": "medium"
              }
            }
          },
          "meeting_preferences": {
            "type": "object",
            "properties": {
              "preferred_times": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Preferred meeting time slots",
                "example": [
                  "09:00-11:00",
                  "14:00-16:00"
                ]
              },
              "buffer_time_minutes": {
                "type": "integer",
                "description": "Buffer time between meetings in minutes",
                "example": 15
              },
              "virtual_meeting_preference": {
                "type": "boolean",
                "description": "Preference for virtual meetings",
                "example": true
              }
            }
          }
        }
      },
      "UserCommunicationSettingsUpdate": {
        "type": "object",
        "description": "Communication settings update payload",
        "properties": {
          "preferred_language": {
            "type": "string"
          },
          "timezone": {
            "type": "string"
          },
          "communication_style": {
            "type": "object",
            "properties": {
              "formality": {
                "type": "string",
                "enum": [
                  "casual",
                  "professional",
                  "formal"
                ]
              },
              "frequency": {
                "type": "string",
                "enum": [
                  "minimal",
                  "normal",
                  "frequent"
                ]
              },
              "channel_preference": {
                "type": "string",
                "enum": [
                  "email",
                  "sms",
                  "in_app",
                  "phone"
                ]
              }
            }
          },
          "ai_assistant": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "personality": {
                "type": "string",
                "enum": [
                  "helpful",
                  "concise",
                  "detailed",
                  "friendly"
                ]
              },
              "proactivity_level": {
                "type": "string",
                "enum": [
                  "low",
                  "medium",
                  "high"
                ]
              }
            }
          },
          "meeting_preferences": {
            "type": "object",
            "properties": {
              "preferred_times": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "buffer_time_minutes": {
                "type": "integer"
              },
              "virtual_meeting_preference": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "MobileAppMetadata": {
        "type": "object",
        "properties": {
          "business": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "example": 123
              },
              "name": {
                "type": "string",
                "example": "Office Chat Solutions"
              },
              "subdomain": {
                "type": "string",
                "example": "officechat"
              },
              "logo_url": {
                "type": "string",
                "nullable": true,
                "example": "https://cdn.workforce.mangoapps.com/logos/officechat.png"
              },
              "timezone": {
                "type": "string",
                "example": "America/New_York"
              },
              "branding": {
                "$ref": "#/components/schemas/Business/properties/branding"
              }
            }
          },
          "login_methods": {
            "type": "object",
            "properties": {
              "email_password": {
                "type": "object",
                "properties": {
                  "enabled": {
                    "type": "boolean",
                    "example": true
                  },
                  "forgot_password_enabled": {
                    "type": "boolean",
                    "example": true
                  }
                }
              },
              "sso_providers": {
                "type": "array",
                "description": "Array of configured SSO providers for this business. Multiple providers of the same\ntype (e.g., multiple Google OAuth2 configurations) are supported. Each provider has\na unique ID that should be passed to the SSO initiate endpoint.\n",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "Unique identifier for this SSO configuration (use as provider_id)",
                      "example": "67"
                    },
                    "provider_type": {
                      "type": "string",
                      "enum": [
                        "google_oauth2",
                        "saml",
                        "entra_id",
                        "mangoapps"
                      ],
                      "example": "google_oauth2"
                    },
                    "provider_name": {
                      "type": "string",
                      "description": "Display name for this SSO configuration",
                      "example": "Google Workspace"
                    },
                    "enabled": {
                      "type": "boolean",
                      "description": "Whether this SSO provider is currently enabled",
                      "example": true
                    },
                    "logo_url": {
                      "type": "string",
                      "description": "URL to provider logo image",
                      "example": "https://cdn.workforce.mangoapps.com/logos/google.png"
                    },
                    "description": {
                      "type": "string",
                      "description": "User-friendly description of this SSO provider",
                      "example": "Sign in with your Google Workspace account"
                    },
                    "channel_support": {
                      "type": "string",
                      "enum": [
                        "web_only",
                        "mobile_only",
                        "both"
                      ],
                      "description": "Where this provider is enabled",
                      "example": "both"
                    },
                    "mobile_supported": {
                      "type": "boolean",
                      "description": "Convenience flag indicating mobile support",
                      "example": true
                    },
                    "web_supported": {
                      "type": "boolean",
                      "description": "Convenience flag indicating web support",
                      "example": true
                    }
                  }
                },
                "example": [
                  {
                    "id": "67",
                    "provider_type": "google_oauth2",
                    "provider_name": "Google Workspace - Corporate",
                    "enabled": true,
                    "logo_url": "https://cdn.workforce.mangoapps.com/logos/google.png",
                    "description": "Sign in with your corporate Google account",
                    "channel_support": "both",
                    "mobile_supported": true,
                    "web_supported": true
                  },
                  {
                    "id": "134",
                    "provider_type": "google_oauth2",
                    "provider_name": "Google Workspace - Partners",
                    "enabled": true,
                    "logo_url": "https://cdn.workforce.mangoapps.com/logos/google.png",
                    "description": "Sign in with your partner Google account",
                    "channel_support": "mobile_only",
                    "mobile_supported": true,
                    "web_supported": false
                  },
                  {
                    "id": "201",
                    "provider_type": "saml",
                    "provider_name": "Okta SAML",
                    "enabled": true,
                    "logo_url": "https://cdn.workforce.mangoapps.com/logos/okta.png",
                    "description": "Sign in with your organization's SAML identity provider",
                    "channel_support": "web_only",
                    "mobile_supported": false,
                    "web_supported": true
                  }
                ]
              },
              "two_factor": {
                "type": "object",
                "properties": {
                  "available": {
                    "type": "boolean",
                    "example": true,
                    "description": "Whether 2FA is available to users"
                  },
                  "enforced": {
                    "type": "boolean",
                    "example": false,
                    "description": "Whether 2FA is required for all users"
                  }
                }
              },
              "biometric_login": {
                "type": "object",
                "properties": {
                  "enabled": {
                    "type": "boolean",
                    "example": true
                  },
                  "methods": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "face_id",
                        "touch_id",
                        "fingerprint"
                      ]
                    },
                    "example": [
                      "face_id",
                      "touch_id",
                      "fingerprint"
                    ]
                  }
                }
              },
              "remember_device": {
                "type": "object",
                "properties": {
                  "enabled": {
                    "type": "boolean",
                    "example": true
                  },
                  "duration_days": {
                    "type": "integer",
                    "example": 30
                  },
                  "api_endpoints": {
                    "type": "object",
                    "properties": {
                      "login": {
                        "type": "string",
                        "example": "/api/v1/auth/login"
                      },
                      "sso_initiate": {
                        "type": "string",
                        "example": "/api/v1/auth/sso/initiate"
                      },
                      "sso_exchange": {
                        "type": "string",
                        "example": "/api/v1/auth/sso/exchange"
                      },
                      "refresh_token": {
                        "type": "string",
                        "example": "/api/v1/auth/refresh"
                      },
                      "logout": {
                        "type": "string",
                        "example": "/api/v1/auth/logout"
                      },
                      "passwordless_request": {
                        "type": "string",
                        "example": "/api/v1/auth/passwordless/request"
                      },
                      "passwordless_verify": {
                        "type": "string",
                        "example": "/api/v1/auth/passwordless/verify"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "InspectionStatus": {
        "type": "string",
        "enum": [
          "draft",
          "scheduled",
          "in_progress",
          "in_review",
          "completed",
          "cancelled"
        ]
      },
      "ComplianceStatus": {
        "type": "string",
        "enum": [
          "pending",
          "passed",
          "failed",
          "na",
          "skipped"
        ]
      },
      "FailureSeverity": {
        "type": "string",
        "enum": [
          "minor",
          "moderate",
          "severe",
          "critical"
        ]
      },
      "InspectionItemType": {
        "type": "string",
        "description": "Renderer hint for the field. Mirrors\n`InspectionTemplateItem#item_type`.\n",
        "enum": [
          "yes_no",
          "checkbox",
          "rating",
          "text",
          "number",
          "slider",
          "datetime",
          "photo",
          "signature",
          "select",
          "multi_select",
          "instruction"
        ]
      },
      "InspectionSummary": {
        "type": "object",
        "description": "Lightweight list row. Source: `InspectionSummarySerializer`.\nReturned by GET /inspections/inspections and the embedded\n`template`/`inspector`/`location` blocks.\n",
        "properties": {
          "id": {
            "type": "integer",
            "example": 421
          },
          "title": {
            "type": "string",
            "example": "Loading dock walkthrough",
            "nullable": true
          },
          "status": {
            "$ref": "#/components/schemas/InspectionStatus"
          },
          "display_status": {
            "type": "string",
            "nullable": true,
            "description": "Human-readable badge label. Normally the lifecycle status\n(\"Draft\"/\"Scheduled\"/\"In Progress\"/\"Passed\"/\"Failed\"/\"Cancelled\"),\nbut returns `\"Overdue\"` when `overdue` is true so the client can\nflag urgency. The raw lifecycle value is always on `status`.\n",
            "example": "In Progress"
          },
          "status_color": {
            "type": "string",
            "description": "Bootstrap contextual color token for the status badge — matches the\nweb UI's `bg-<status_color>` class so native clients render the same\nchip coloring. Returns `danger` when `overdue` is true.\n",
            "enum": [
              "secondary",
              "info",
              "warning",
              "success",
              "danger",
              "dark"
            ],
            "example": "warning"
          },
          "passed": {
            "type": "boolean",
            "nullable": true
          },
          "score": {
            "type": "number",
            "nullable": true,
            "example": 92.5
          },
          "template": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer",
                "example": 12
              },
              "name": {
                "type": "string",
                "example": "General Safety Inspection"
              },
              "category": {
                "type": "string",
                "nullable": true,
                "example": "safety"
              }
            }
          },
          "inspector": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer",
                "example": 8
              },
              "name": {
                "type": "string",
                "example": "Jane Doe"
              },
              "photo_url": {
                "type": "string",
                "description": "Avatar URL for the inspector (web-parity). Falls back to a\ngenerated ui-avatars.com initial tile when no profile photo\nis attached, so this field is always present and renderable.\n",
                "example": "/rails/active_storage/representations/redirect/.../avatar.jpg"
              }
            }
          },
          "location": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer",
                "example": 3
              },
              "name": {
                "type": "string",
                "example": "Plant 3"
              }
            }
          },
          "due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "started_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "progress_percentage": {
            "type": "integer",
            "nullable": true,
            "example": 67
          },
          "overdue": {
            "type": "boolean",
            "example": false
          },
          "review_reason": {
            "type": "string",
            "nullable": true,
            "description": "Latest approve/reject reviewer comment on the inspection's approval\nworkflow, so a list row can show why it was approved or returned for\nrework. Null when there is no decision yet (pending review, never\nreviewed, or no workflow attached). Source: `ApprovalAction#comments`.\n"
          },
          "current_level": {
            "type": "integer",
            "nullable": true,
            "description": "1-based approval level the request currently sits at. Non-null only\nwhile the inspection is awaiting a decision (`in_review`); null\notherwise.\n",
            "example": 1
          },
          "pending_with": {
            "type": "string",
            "nullable": true,
            "description": "Display name of the level the inspection is awaiting a decision at\n(e.g. \"Manager Review\"), so the reviewer queue can render\n\"Pending with <level>\" without a detail fetch. Falls back to\n\"Level N\" when the level has no name. Null when not awaiting a\ndecision.\n",
            "example": "Manager Review"
          }
        },
        "required": [
          "id",
          "status",
          "created_at",
          "updated_at"
        ]
      },
      "InspectionLocationFilter": {
        "type": "object",
        "description": "Team-feed location dropdown payload. Source:\n`Api::V1::Inspections::InspectionsController#team_location_filter`,\nbacked by `Inspections::TeamScopeService#available_groups`. Surfaced as\nthe top-level `location_filter` key on `GET /inspections/inspections`\nwhen `team=true`. `options` is the permission-scoped set the caller may\nfilter by (their location assignments plus locations of inspections they\ncan already see) — not merely the locations present on the current page.\n",
        "properties": {
          "axis": {
            "type": "string",
            "description": "The tenant's `team_inspections_scope`. `options` is only a meaningful\nlocation list when this is `location`; for `department`/`none` the\nclient should hide the location filter.\n",
            "enum": [
              "location",
              "department",
              "none"
            ],
            "example": "location"
          },
          "selected_id": {
            "type": "integer",
            "nullable": true,
            "description": "The currently-applied `location_id`, echoed back. Null for the default \"all my locations\" view.",
            "example": 3
          },
          "options": {
            "type": "array",
            "description": "Locations the caller may filter by. Render after an \"All my locations\" sentinel.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 3
                },
                "name": {
                  "type": "string",
                  "example": "Plant 3"
                }
              }
            }
          }
        },
        "required": [
          "axis",
          "options"
        ]
      },
      "InspectionItemMediaItem": {
        "type": "object",
        "description": "Polymorphic `MediaItem` row attached to an inspection item — photo\nOR video. `media_kind` distinguishes the two. Source:\n`InspectionItemSerializer#media_item_payload`. Replaces the legacy\n`InspectionItemPhoto` / `InspectionItemVideo` blob shapes (which\nflattened Active Storage blobs); evidence now flows through the\n`MediaItem` model and is uploaded via the direct-upload + attach\nflow described in `POST /api/v1/inspections/uploads/direct`.\n",
        "properties": {
          "id": {
            "type": "integer"
          },
          "media_kind": {
            "type": "string",
            "enum": [
              "photo",
              "video"
            ],
            "example": "photo"
          },
          "content_type": {
            "type": "string",
            "example": "image/jpeg"
          },
          "byte_size": {
            "type": "integer",
            "example": 2097152
          },
          "original_filename": {
            "type": "string",
            "nullable": true,
            "example": "photo-1716499200.jpg"
          },
          "url": {
            "type": "string",
            "example": "/rails/active_storage/blobs/redirect/abc/photo.jpg"
          },
          "thumb_url": {
            "type": "string",
            "nullable": true,
            "example": "/rails/active_storage/representations/.../photo.jpg"
          },
          "has_annotations": {
            "type": "boolean",
            "example": false
          },
          "captured_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        },
        "required": [
          "id",
          "media_kind",
          "content_type",
          "byte_size",
          "url"
        ]
      },
      "InspectionItemTemplateSnapshot": {
        "type": "object",
        "description": "Subset of the parent template-item fields the inspection-item\nserializer inlines so the renderer doesn't need a second fetch.\n",
        "properties": {
          "id": {
            "type": "integer"
          },
          "item_type": {
            "$ref": "#/components/schemas/InspectionItemType"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "requires_photo": {
            "type": "boolean"
          },
          "invert_yes_no": {
            "type": "boolean",
            "example": false,
            "description": "When true the yes/no question is inverted — \"No\" is the compliant\nanswer and \"Yes\" fails. Renderers flip the answer chips and the\nscoring engine flips pass/fail.\n"
          },
          "min_value": {
            "type": "number",
            "nullable": true
          },
          "max_value": {
            "type": "number",
            "nullable": true
          },
          "options": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true
          },
          "form_conditions": {
            "$ref": "#/components/schemas/InspectionFormConditions"
          }
        }
      },
      "InspectionItem": {
        "type": "object",
        "description": "One row from the inspection's `items` array. Source:\n`InspectionItemSerializer`. The `template_item` block carries the\nitem-type metadata; `compliance_status` carries the user's answer.\n",
        "properties": {
          "id": {
            "type": "integer"
          },
          "inspection_id": {
            "type": "integer"
          },
          "template_item_id": {
            "type": "integer",
            "nullable": true
          },
          "name": {
            "type": "string",
            "example": "Tires properly inflated"
          },
          "section_name": {
            "type": "string",
            "nullable": true,
            "example": "Pre-Start Safety"
          },
          "position": {
            "type": "integer",
            "example": 0
          },
          "weight": {
            "type": "number",
            "example": 1.0
          },
          "critical": {
            "type": "boolean",
            "example": false
          },
          "compliance_status": {
            "$ref": "#/components/schemas/ComplianceStatus"
          },
          "response_value": {
            "type": "string",
            "nullable": true,
            "example": "Yes"
          },
          "notes": {
            "type": "string",
            "nullable": true
          },
          "failure_severity": {
            "$ref": "#/components/schemas/FailureSeverity",
            "nullable": true
          },
          "score": {
            "type": "number",
            "nullable": true,
            "example": 0.95
          },
          "responded_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "template_item": {
            "$ref": "#/components/schemas/InspectionItemTemplateSnapshot",
            "nullable": true
          },
          "media_items": {
            "type": "array",
            "description": "Photos + videos attached to this item, in `MediaItem#ordered`\nsequence (the same order the web inspector renders). Photos\nand videos are intermixed; filter client-side by `media_kind`\nif a kind-specific view is needed.\n",
            "items": {
              "$ref": "#/components/schemas/InspectionItemMediaItem"
            }
          },
          "media_count": {
            "type": "integer",
            "example": 2,
            "description": "Total media (photos + videos)."
          },
          "photo_count": {
            "type": "integer",
            "example": 2
          },
          "video_count": {
            "type": "integer",
            "example": 0
          }
        },
        "required": [
          "id",
          "inspection_id",
          "name",
          "position",
          "compliance_status",
          "updated_at"
        ]
      },
      "InspectionGps": {
        "type": "object",
        "description": "GPS capture stamped on the inspection at submit time.",
        "properties": {
          "latitude": {
            "type": "number",
            "nullable": true,
            "example": 40.7128
          },
          "longitude": {
            "type": "number",
            "nullable": true,
            "example": -74.006
          },
          "accuracy": {
            "type": "number",
            "nullable": true,
            "example": 5.2
          },
          "address": {
            "type": "string",
            "nullable": true,
            "example": "New York, NY"
          },
          "captured_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "InspectionApproval": {
        "type": "object",
        "description": "Approval-workflow state when the inspection is `in_review`. The whole\nblock is omitted for a non-reviewer's pending request; `can_review` is\ntrue only for an eligible reviewer at the current level.\n",
        "properties": {
          "id": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "approved",
              "rejected",
              "escalated",
              "auto_approved",
              "cancelled"
            ]
          },
          "current_level": {
            "type": "integer",
            "example": 1
          },
          "requested_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "workflow_id": {
            "type": "integer",
            "nullable": true
          },
          "can_review": {
            "type": "boolean",
            "description": "True when the current caller may approve/reject at the current\nlevel. Gate the Approve/Reject affordances on this flag\n(server-authoritative), not on client-side role inference.\n",
            "example": true
          },
          "last_action": {
            "type": "object",
            "nullable": true,
            "description": "Most recent decision (approve/reject) on this request; null before any decision.",
            "properties": {
              "action": {
                "type": "string",
                "enum": [
                  "approved",
                  "rejected"
                ],
                "example": "rejected"
              },
              "reason": {
                "type": "string",
                "nullable": true,
                "description": "Reviewer comment (required on reject)."
              },
              "acted_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "acted_by": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      },
      "InspectionUserRef": {
        "type": "object",
        "nullable": true,
        "description": "Lightweight user reference. `name` is the user's display name; null when no actor is recorded.",
        "properties": {
          "id": {
            "type": "integer",
            "nullable": true
          },
          "name": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "InspectionReviewTimeline": {
        "type": "object",
        "description": "Read-only review timeline — the same submitted → decisions → awaiting →\nupcoming → terminal sequence the desktop \"Review Timeline\" card renders,\nplus prior rework rounds. Unlike `approval` (reviewer-gated for action\naffordances), this block is visible to everyone who can view the\ninspection. The whole object is null when the inspection never entered\nreview and carries no workflow_history audit trail.\n\nTwo modes:\n  - Approval-backed: `workflow` is present and `nodes` use the typed\n    entries below (submitted/decision/awaiting/upcoming/terminal).\n  - Legacy / no-workflow completion: `workflow`, `status`, and\n    `current_level` are null and `nodes` are `event` entries built from\n    the inspection's workflow_history audit trail.\n",
        "properties": {
          "status": {
            "type": "string",
            "nullable": true,
            "enum": [
              "pending",
              "approved",
              "rejected",
              "escalated",
              "auto_approved",
              "cancelled"
            ],
            "description": "Latest ApprovalRequest status; null in the workflow_history fallback."
          },
          "current_level": {
            "type": "integer",
            "nullable": true
          },
          "workflow": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "max_levels": {
                "type": "integer",
                "example": 2
              }
            }
          },
          "nodes": {
            "type": "array",
            "description": "Ordered timeline entries. Render by `type`.",
            "items": {
              "type": "object",
              "description": "One of six shapes, discriminated by `type`:\n  - submitted: { type, label, by, at }\n  - decision:  { type, status, label, level_number, level_name, by, at, response_time_hours, comments }\n  - awaiting:  { type, label, level_number, level_name, step, total_steps, eligible_reviewers[] }\n  - upcoming:  { type, label, level_number, level_name }\n  - terminal:  { type, status, label, completed_at }\n  - event:     { type, action, label, by, at, comments }  # workflow_history fallback\n",
              "properties": {
                "type": {
                  "type": "string",
                  "enum": [
                    "submitted",
                    "decision",
                    "awaiting",
                    "upcoming",
                    "terminal",
                    "event"
                  ]
                },
                "label": {
                  "type": "string"
                },
                "status": {
                  "type": "string",
                  "nullable": true,
                  "description": "decision: approved|rejected|escalated|auto_approved · terminal: approved|rejected|escalated|cancelled"
                },
                "action": {
                  "type": "string",
                  "nullable": true,
                  "description": "Raw action for `event` nodes (e.g. submitted, completed, returned)."
                },
                "level_number": {
                  "type": "integer",
                  "nullable": true
                },
                "level_name": {
                  "type": "string",
                  "nullable": true
                },
                "step": {
                  "type": "integer",
                  "nullable": true
                },
                "total_steps": {
                  "type": "integer",
                  "nullable": true
                },
                "response_time_hours": {
                  "type": "number",
                  "nullable": true,
                  "example": 14.73
                },
                "comments": {
                  "type": "string",
                  "nullable": true,
                  "description": "Null when blank — never an empty string."
                },
                "completed_at": {
                  "type": "string",
                  "format": "date-time",
                  "nullable": true
                },
                "at": {
                  "type": "string",
                  "format": "date-time",
                  "nullable": true
                },
                "by": {
                  "$ref": "#/components/schemas/InspectionUserRef"
                },
                "eligible_reviewers": {
                  "type": "array",
                  "description": "Present on `awaiting` nodes — all reviewers eligible at the current level.",
                  "items": {
                    "$ref": "#/components/schemas/InspectionUserRef"
                  }
                }
              }
            }
          },
          "previous_rounds": {
            "type": "array",
            "description": "Earlier rework loops, most-recent first. Empty in the fallback.",
            "items": {
              "type": "object",
              "properties": {
                "round": {
                  "type": "integer",
                  "description": "Chronological index (1 = oldest)."
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "pending",
                    "approved",
                    "rejected",
                    "escalated",
                    "auto_approved",
                    "cancelled"
                  ]
                },
                "submitted_by": {
                  "$ref": "#/components/schemas/InspectionUserRef"
                },
                "submitted_at": {
                  "type": "string",
                  "format": "date-time",
                  "nullable": true
                },
                "actions": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "action": {
                        "type": "string",
                        "example": "rejected"
                      },
                      "by": {
                        "$ref": "#/components/schemas/InspectionUserRef"
                      },
                      "level_number": {
                        "type": "integer",
                        "nullable": true
                      },
                      "level_name": {
                        "type": "string",
                        "nullable": true
                      },
                      "comments": {
                        "type": "string",
                        "nullable": true
                      },
                      "acted_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "InspectionAsset": {
        "type": "object",
        "description": "AssetPro asset (equipment) linked to the inspection.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "nullable": true,
            "example": "Toyota 8FGCU25 — FL-04"
          },
          "condition": {
            "type": "string",
            "nullable": true,
            "enum": [
              "excellent",
              "good",
              "fair",
              "poor",
              "damaged",
              "unknown"
            ]
          }
        }
      },
      "InspectionMetadata": {
        "type": "object",
        "description": "Sanitized client-side metadata (the full `metadata` JSONB is not\nsurfaced to mobile to keep the workflow_history audit log private).\n",
        "properties": {
          "device_info": {
            "type": "string",
            "nullable": true
          },
          "app_version": {
            "type": "string",
            "nullable": true
          },
          "offline_created": {
            "type": "boolean",
            "nullable": true
          },
          "synced_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "weather_conditions": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "InspectionCorrectiveActionUser": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "example": "Jane Doe"
          }
        }
      },
      "InspectionCorrectiveAction": {
        "type": "object",
        "description": "Auto-generated follow-up task created from a failed item. Source:\n`CorrectiveActionSerializer`. Links back to the inspection item\nthat produced it; can also be linked to the Tasks app via `task_id`.\n",
        "properties": {
          "id": {
            "type": "integer"
          },
          "inspection_id": {
            "type": "integer"
          },
          "inspection_item_id": {
            "type": "integer",
            "nullable": true
          },
          "description": {
            "type": "string",
            "example": "Replace damaged eyewash bottle."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "in_progress",
              "completed",
              "cancelled"
            ]
          },
          "priority": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high",
              "critical"
            ]
          },
          "due_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "resolution_notes": {
            "type": "string",
            "nullable": true
          },
          "assigned_to": {
            "$ref": "#/components/schemas/InspectionCorrectiveActionUser",
            "nullable": true
          },
          "created_by": {
            "$ref": "#/components/schemas/InspectionCorrectiveActionUser",
            "nullable": true
          },
          "inspection": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "title": {
                "type": "string",
                "nullable": true
              },
              "url": {
                "type": "string",
                "description": "Mobile-web deep link (`/m/apps/inspections/<id>`) — native\nclients open the inspection read-only inside the in-app\nWebView, matching the inspections list `url`.\n"
              }
            }
          },
          "inspection_item": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "section_name": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "overdue": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "InspectionDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/InspectionSummary"
          },
          {
            "type": "object",
            "description": "Full inspection payload used by show / create / complete / cancel.\nAdds notes, GPS, items, corrective actions, approval state, and\nsanitized metadata. Source: `InspectionDetailSerializer`.\n",
            "properties": {
              "notes": {
                "type": "string",
                "nullable": true
              },
              "summary": {
                "type": "string",
                "nullable": true
              },
              "ai_summary": {
                "type": "string",
                "nullable": true,
                "description": "AI-generated executive summary stamped on submit. Null until the\nbackground summarizer finishes, or when AI summarization is\ndisabled for the tenant.\n"
              },
              "inspector_signature": {
                "type": "string",
                "nullable": true,
                "description": "Stored URL or data-uri for the captured signature."
              },
              "source_type": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "manual",
                  "scheduled",
                  "asset_requirement"
                ]
              },
              "duration_seconds": {
                "type": "integer",
                "nullable": true
              },
              "gps": {
                "$ref": "#/components/schemas/InspectionGps"
              },
              "score_breakdown": {
                "type": "object",
                "nullable": true,
                "additionalProperties": true
              },
              "sections": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "items": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/InspectionItem"
                }
              },
              "corrective_actions": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/InspectionCorrectiveAction"
                }
              },
              "approval": {
                "$ref": "#/components/schemas/InspectionApproval",
                "nullable": true
              },
              "review_timeline": {
                "$ref": "#/components/schemas/InspectionReviewTimeline",
                "nullable": true
              },
              "asset": {
                "$ref": "#/components/schemas/InspectionAsset",
                "nullable": true
              },
              "metadata": {
                "$ref": "#/components/schemas/InspectionMetadata"
              },
              "can_delete": {
                "type": "boolean",
                "description": "True when the current caller may delete this inspection. Reflects\nBOTH gates the DELETE endpoint enforces: the per-business\n`allow_delete_inspections` setting (off by default) AND modify\nauthority (the assigned inspector or a manager/inspections-admin).\nFalse when deletion is disabled business-wide, so the native\nclient should hide the Delete affordance. Emitted on the show\npayload only. Source: `InspectionsController#can_delete_inspection?`.\n"
              },
              "can_take_over": {
                "type": "boolean",
                "description": "True when the current caller may take over this in-progress\ninspection (or pick up a scheduled one, when the tenant allows\npickup) — i.e. a manager/admin or a peer assigned to the\ninspection's location who is not already the inspector. Drives\nthe native \"Continue\"/\"Pick up this inspection\" affordance.\nSource: `Inspection#takeable_by?`.\n"
              },
              "can_review": {
                "type": "boolean",
                "description": "True when the current caller may approve/reject this inspection\nright now — i.e. it is in review and the caller is an eligible\nreviewer at the current approval level (submitter excluded by\nseparation of duties). Drives the native \"Review\" affordance.\nAlways present on the detail payload (false when nothing is in\nreview or the caller isn't eligible) — unlike the reviewer-gated\n`approval.can_review`, which is omitted with the whole `approval`\nblock for a non-reviewer's pending request. Source:\n`InspectionDetailSerializer#can_review?` →\n`ApprovalLevel#can_approve?`.\n"
              },
              "last_activity_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "Most-recent activity across the inspection record, its items\n(status/response/notes edits), and their media. The \"Last\nupdated\" recency signal for the takeover decision — reliable\nwhere `updated_at` alone is not, since item edits don't touch\nthe parent inspection. Source: `Inspection#last_activity_at`.\n"
              },
              "template": {
                "type": "object",
                "nullable": true,
                "description": "On the detail payload the nested template block is enriched\n(beyond the summary's id/name/category) with `require_signature`\nand the per-template failure follow-up prompt config, so the\nconduct/complete screen can decide whether to prompt for a\nsignature and drive the failure-capture modal without a second\ncall to the template detail/bundle endpoint.\nSource: `InspectionDetailSerializer`.\n",
                "properties": {
                  "id": {
                    "type": "integer",
                    "example": 12
                  },
                  "name": {
                    "type": "string",
                    "example": "General Safety Inspection"
                  },
                  "category": {
                    "type": "string",
                    "nullable": true,
                    "example": "safety"
                  },
                  "require_signature": {
                    "type": "boolean",
                    "description": "Whether the inspector must capture a digital signature\nbefore completing. When false the native client skips the\nsignature step. Same canonical\n`InspectionTemplate#require_signature?` predicate the\ntemplate summary/detail payloads expose.\n"
                  },
                  "failure_prompt_settings": {
                    "type": "object",
                    "nullable": true,
                    "description": "Per-template controls for the failure-capture modal:\n`prompt_enabled` (boolean master toggle) plus `prompt_photo`,\n`prompt_severity`, `prompt_comment`, `prompt_action_item`\n(each \"off\" | \"optional\" | \"required\"). Null when the\ninspection has no template; an empty object `{}` when the\ntemplate exists but was never configured — clients resolve a\nmissing master → false and any missing field → \"off\".\n",
                    "additionalProperties": true
                  }
                }
              }
            }
          }
        ]
      },
      "InspectionTemplateSummary": {
        "type": "object",
        "description": "Compact template row for the picker. Source:\n`TemplateSummarySerializer`.\n",
        "properties": {
          "id": {
            "type": "integer",
            "example": 12
          },
          "name": {
            "type": "string",
            "example": "Forklift Daily Pre-Use"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "example": "Daily pre-shift safety check…"
          },
          "category": {
            "type": "string",
            "nullable": true,
            "example": "safety"
          },
          "inspection_type": {
            "type": "string",
            "nullable": true,
            "example": "equipment"
          },
          "pass_threshold": {
            "type": "number",
            "nullable": true,
            "example": 80.0
          },
          "estimated_duration_minutes": {
            "type": "integer",
            "nullable": true,
            "description": "Approximate time to complete the inspection (minutes). Authored on\nthe template; surfaced on the list row so the picker can show\n\"~15 min\" without a follow-up detail call.\n",
            "example": 15
          },
          "item_count": {
            "type": "integer",
            "example": 13
          },
          "inspections_count": {
            "type": "integer",
            "example": 142
          },
          "require_signature": {
            "type": "boolean"
          },
          "requires_training": {
            "type": "boolean"
          },
          "has_approval_workflow": {
            "type": "boolean"
          },
          "active": {
            "type": "boolean"
          },
          "is_system_template": {
            "type": "boolean"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "name",
          "active",
          "updated_at"
        ]
      },
      "InspectionTemplateItem": {
        "type": "object",
        "description": "One field in a template definition. Source: the inline\n`template_item_payload` in `TemplateDetailSerializer`.\n",
        "properties": {
          "id": {
            "type": "integer"
          },
          "position": {
            "type": "integer",
            "example": 0
          },
          "section_name": {
            "type": "string",
            "nullable": true,
            "example": "Pre-Start Safety Checks"
          },
          "name": {
            "type": "string",
            "example": "Tires properly inflated"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "item_type": {
            "$ref": "#/components/schemas/InspectionItemType"
          },
          "weight": {
            "type": "number",
            "example": 1.0
          },
          "critical": {
            "type": "boolean",
            "example": false
          },
          "requires_photo": {
            "type": "boolean",
            "example": false
          },
          "auto_fail_on_no": {
            "type": "boolean",
            "example": false
          },
          "invert_yes_no": {
            "type": "boolean",
            "example": false,
            "description": "When true the yes/no question is inverted — \"No\" is the compliant\nanswer and \"Yes\" fails. Renderers flip the answer chips and the\nscoring engine flips pass/fail.\n"
          },
          "min_value": {
            "type": "number",
            "nullable": true
          },
          "max_value": {
            "type": "number",
            "nullable": true
          },
          "options": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "Item-type-specific configuration. For `select`/`multi_select`\nthis is `{ choices: [{ value, label }] }`. For `number`/`rating`\nit may include `unit`, `step`, etc. For `slider` it pairs with\n`min_value`/`max_value`.\n"
          },
          "form_conditions": {
            "$ref": "#/components/schemas/InspectionFormConditions"
          }
        },
        "required": [
          "id",
          "position",
          "name",
          "item_type"
        ]
      },
      "InspectionFormConditions": {
        "type": "object",
        "nullable": true,
        "additionalProperties": true,
        "description": "FormKit conditional-logic contract for a branching template item, or\nnull for unconditional items. Shape:\n\n    {\n      \"show_when\":    [ { \"field\": \"item_<parent_template_item_id>\",\n                          \"operator\": \"equals\" | \"in_list\",\n                          \"value\": \"yes\" | [\"opt_a\", \"opt_b\"] } ],\n      \"hide_when\":    [],\n      \"require_when\": [ ...same rule shape... ]\n    }\n\nEvaluation semantics (mirror the server's FormKit::ConditionEvaluator\nand the web client's form_conditions.js — clients must match):\n- `field` refers to another item in the same template, keyed\n  `item_<template_item_id>`. Compare against the item's ANSWER value —\n  for `yes_no`/`checkbox` items that is the raw `\"yes\"`/`\"no\"`/`\"na\"`\n  string (NOT compliance_status; inverted items still answer\n  \"yes\"/\"no\"), for every other type the literal `response_value`;\n  unanswered compares as `\"\"`.\n- Multiple rules in one array are AND-ed.\n- Visibility: non-empty `show_when` decides; else non-empty\n  `hide_when` inverts; else visible. A hidden item is never required,\n  is excluded from scoring/progress, and does not block completion.\n  Hidden items' answers are treated as blank when evaluating OTHER\n  items' rules (chained conditions).\n- Required: non-empty `require_when` decides (only while visible);\n  otherwise the item is required by default.\n- The server currently emits only `equals` and `in_list`, but the\n  evaluator accepts the full operator set: equals, not_equals,\n  contains, not_contains, greater_than, less_than,\n  greater_than_or_equal, less_than_or_equal, is_empty, is_not_empty,\n  in_list, not_in_list, starts_with, ends_with, matches_pattern.\n  Numeric operators coerce with Ruby `to_f` semantics\n  (nil/blank/non-numeric → 0). Unknown operators evaluate false.\n- String comparisons are CASE-INSENSITIVE: `equals`/`not_equals`/\n  `in_list`/`not_in_list` also trim surrounding whitespace;\n  `contains`/`starts_with`/`ends_with` fold case only.\n  `matches_pattern` stays case-sensitive. Clients must implement the\n  same folding.\n- A `multi_select` parent's answer evaluates as an ARRAY of the\n  selected values: `equals`/`contains`/`in_list` match when ANY\n  selected value matches (server and clients agree on this).\n"
      },
      "InspectionTemplateDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/InspectionTemplateSummary"
          },
          {
            "type": "object",
            "description": "Full template payload used by GET /templates/:id. Adds the\nitems array, sections order, estimated duration, and the\ntemplate-level failure-prompt settings the renderer uses to\ndrive the failure modal.\n",
            "properties": {
              "description": {
                "type": "string",
                "nullable": true
              },
              "estimated_duration_minutes": {
                "type": "integer",
                "nullable": true
              },
              "sections_order": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "failure_prompt_settings": {
                "type": "object",
                "nullable": true,
                "description": "Per-template controls for the failure-capture modal.\nKeys map to `prompt_enabled`, `prompt_photo`, `prompt_severity`,\n`prompt_comment`, `prompt_action_item`.\n",
                "additionalProperties": true
              },
              "max_photos_per_item": {
                "type": "integer",
                "nullable": true,
                "description": "App-level cap (Evidence & Input setting) on photos per\ninspection item. Null when unset; clients should apply their\nown default. Same value for every item in the template.\n"
              },
              "items": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/InspectionTemplateItem"
                }
              }
            }
          }
        ]
      },
      "InspectionCreateItem": {
        "type": "object",
        "description": "Item update payload submitted as part of `POST /inspections`. The\nserver matches it to an existing `inspection_item` by `id`\n(preferred — used in the sync flow) or by `template_item_id`\n(used in the create-from-template flow, where the inspection_items\nare auto-created by the model's `after_create` callback).\n",
        "properties": {
          "id": {
            "type": "integer",
            "nullable": true,
            "description": "Inspection item id (sync flow only)."
          },
          "template_item_id": {
            "type": "integer",
            "nullable": true,
            "description": "Template item id (create flow)."
          },
          "compliance_status": {
            "$ref": "#/components/schemas/ComplianceStatus"
          },
          "response_value": {
            "type": "string",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "nullable": true
          },
          "failure_severity": {
            "$ref": "#/components/schemas/FailureSeverity",
            "nullable": true
          }
        }
      },
      "InspectionCreateMediaUpload": {
        "type": "object",
        "description": "Refers to an Active Storage blob the client already PUT to S3 via\n`/inspections/direct_uploads`. Attaches it to a specific\ninspection item once the inspection is created. Used when the\nclient wants to upload media before knowing the inspection id —\nin the normal flow callers use the per-item photos endpoint instead.\n",
        "properties": {
          "item_id": {
            "type": "integer"
          },
          "blob_signed_id": {
            "type": "string",
            "example": "eyJfcmFpbHMi..."
          },
          "kind": {
            "type": "string",
            "enum": [
              "photo",
              "video"
            ],
            "default": "photo"
          }
        }
      },
      "InspectionCreateRequest": {
        "type": "object",
        "description": "Body for `POST /inspections/inspections`.",
        "properties": {
          "inspection_template_id": {
            "type": "integer",
            "example": 12
          },
          "title": {
            "type": "string",
            "nullable": true,
            "example": "Forklift FL-04 daily check"
          },
          "location_id": {
            "type": "integer",
            "nullable": true
          },
          "asset_pro_asset_id": {
            "type": "integer",
            "nullable": true
          },
          "due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "nullable": true
          },
          "gps_latitude": {
            "type": "number",
            "nullable": true
          },
          "gps_longitude": {
            "type": "number",
            "nullable": true
          },
          "gps_accuracy": {
            "type": "number",
            "nullable": true
          },
          "gps_address": {
            "type": "string",
            "nullable": true
          },
          "gps_captured_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "items": {
            "type": "array",
            "description": "Optional batch of item updates (offline submissions).",
            "items": {
              "$ref": "#/components/schemas/InspectionCreateItem"
            }
          },
          "media_uploads": {
            "type": "array",
            "description": "Optional blobs to attach to specific inspection items.",
            "items": {
              "$ref": "#/components/schemas/InspectionCreateMediaUpload"
            }
          },
          "complete": {
            "type": "boolean",
            "default": false,
            "description": "When true, the server calls `submit!` after applying item updates."
          },
          "complete_notes": {
            "type": "string",
            "nullable": true,
            "description": "Final notes to record on the submission (used when `complete=true`)."
          }
        },
        "required": [
          "inspection_template_id"
        ]
      },
      "InspectionUpdateRequest": {
        "type": "object",
        "description": "Body for `PATCH /inspections/inspections/:id`. Mirrors the desktop Edit\nInspection form's field set exactly. Accepts the fields at the top\nlevel or nested under an `inspection: { ... }` key so callers can use\neither shape. `inspection_template_id` is silently rejected\n(422 `template_locked`) when the inspection is `in_progress` or\n`completed` — matches the web form, where the template select is\ndisabled in those states.\n",
        "properties": {
          "title": {
            "type": "string",
            "nullable": true
          },
          "inspection_template_id": {
            "type": "integer",
            "nullable": true,
            "description": "Locked when the inspection is in_progress or completed."
          },
          "location_id": {
            "type": "integer",
            "nullable": true
          },
          "scheduled_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "InspectionCorrectiveActionCreateItem": {
        "type": "object",
        "description": "Follow-up action carried inline on the `sync` / `complete` body.\nEach entry is either an UPDATE (when `id` is supplied — must reference\na corrective action already on this inspection) or a CREATE (any other\ncase). Validation mirrors `POST /inspections/inspections/:id/corrective_actions`:\ncross-tenant `assigned_to_id`, off-inspection `inspection_item_id`,\nand out-of-allowlist priority/status all 422 the whole sync /\ncomplete request (atomic — no partial writes).\n",
        "properties": {
          "id": {
            "type": "integer",
            "nullable": true,
            "description": "Existing inspection_corrective_action id (update mode)."
          },
          "description": {
            "type": "string",
            "description": "Required on create. Max 2000 chars."
          },
          "priority": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high",
              "critical"
            ],
            "default": "medium"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "in_progress",
              "completed",
              "cancelled"
            ],
            "default": "pending"
          },
          "due_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "assigned_to_id": {
            "type": "integer",
            "nullable": true,
            "description": "Must be a member of the calling business."
          },
          "inspection_item_id": {
            "type": "integer",
            "nullable": true,
            "description": "Must belong to the inspection being synced/completed."
          },
          "resolution_notes": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "InspectionCompleteRequest": {
        "type": "object",
        "description": "Body for `POST /inspections/inspections/:id/complete`.",
        "properties": {
          "signature": {
            "type": "string",
            "nullable": true,
            "description": "Inspector signature, base64-encoded PNG. May be a full data\nURI (`data:image/png;base64,...`) or a bare base64 string.\n"
          },
          "notes": {
            "type": "string",
            "nullable": true,
            "description": "Final inspector notes to attach to the submission."
          },
          "corrective_actions": {
            "type": "array",
            "description": "Optional follow-up actions to create (or update) atomically as part\nof the completion. Persisted BEFORE the state transition so the\npost-completion fan-out (`FormIntegrationService`) sees them and\nskips auto-creating duplicates for the same failed items.\n",
            "items": {
              "$ref": "#/components/schemas/InspectionCorrectiveActionCreateItem"
            }
          }
        }
      },
      "InspectionCancelRequest": {
        "type": "object",
        "description": "Body for `POST /inspections/inspections/:id/cancel`.",
        "properties": {
          "reason": {
            "type": "string",
            "nullable": true,
            "description": "Free-form reason recorded in the audit log."
          }
        }
      },
      "InspectionTakeOverRequest": {
        "type": "object",
        "description": "Body for `POST /inspections/inspections/:id/take_over`.",
        "properties": {
          "reason": {
            "type": "string",
            "nullable": true,
            "description": "Free-form reason recorded in the reassignment audit entry."
          }
        }
      },
      "InspectionSyncRequest": {
        "type": "object",
        "description": "Body for `POST /inspections/inspections/:id/sync`. Applies a batch of\nitem updates + inspection-level patch fields to an in-progress\ninspection without completing it (unless `complete: true`).\n",
        "properties": {
          "client_id": {
            "type": "string",
            "nullable": true,
            "description": "Client-side idempotency token (typically a UUID)."
          },
          "client_updated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Client-side timestamp for last-write-wins conflict resolution."
          },
          "inspection": {
            "$ref": "#/components/schemas/InspectionSyncRequestInspection"
          },
          "items": {
            "type": "array",
            "description": "Item updates to apply (located by `id` or `template_item_id`).",
            "items": {
              "$ref": "#/components/schemas/InspectionCreateItem"
            }
          },
          "media_uploads": {
            "type": "array",
            "description": "Direct-upload blob refs to attach to specific items.",
            "items": {
              "$ref": "#/components/schemas/InspectionCreateMediaUpload"
            }
          },
          "corrective_actions": {
            "type": "array",
            "description": "Optional follow-up actions to create (or update) atomically in\nthe same transaction as the item / media writes. A validation\nfailure on ANY entry rolls back the entire sync (no partial\nwrites). When the sync also carries `complete: true`, CAs land\nBEFORE `submit!` so the post-completion fan-out skips creating\nduplicates for the same failed items.\n",
            "items": {
              "$ref": "#/components/schemas/InspectionCorrectiveActionCreateItem"
            }
          },
          "complete": {
            "type": "boolean",
            "default": false,
            "description": "When true, call `submit!` after applying the batch."
          }
        }
      },
      "InspectionSyncRequestInspection": {
        "type": "object",
        "description": "Inspection-level patch carried inside `InspectionSyncRequest.inspection`.\nMirrors the whitelist applied by `apply_inspection_updates!` on the\nserver. `signature` is read here (not at the top level) and forwarded\nto `submit!` when `complete: true`.\n",
        "properties": {
          "notes": {
            "type": "string",
            "nullable": true
          },
          "summary": {
            "type": "string",
            "nullable": true
          },
          "signature": {
            "type": "string",
            "nullable": true,
            "description": "Base64 or stored ref; only consumed when `complete: true`."
          },
          "gps_latitude": {
            "type": "number",
            "nullable": true
          },
          "gps_longitude": {
            "type": "number",
            "nullable": true
          },
          "gps_accuracy": {
            "type": "number",
            "nullable": true
          },
          "gps_address": {
            "type": "string",
            "nullable": true
          },
          "gps_captured_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "InspectionCorrectiveActionUpdateRequest": {
        "type": "object",
        "description": "Body for `PATCH /inspections/corrective_actions/:id`. Every field\nis optional; only the keys present are applied.\n",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "in_progress",
              "completed",
              "cancelled"
            ]
          },
          "priority": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high",
              "critical"
            ]
          },
          "description": {
            "type": "string"
          },
          "due_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "assigned_to_id": {
            "type": "integer",
            "nullable": true
          },
          "resolution_notes": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "DirectUploadBlobRequest": {
        "type": "object",
        "description": "Pre-upload blob descriptor. The client SHA256-hashes the raw bytes\nand sends the digest as `checksum` so Active Storage can verify\nthe eventual PUT to S3.\n",
        "properties": {
          "blob": {
            "type": "object",
            "required": [
              "filename",
              "content_type",
              "byte_size",
              "checksum"
            ],
            "properties": {
              "filename": {
                "type": "string",
                "example": "photo-1716499200.jpg"
              },
              "content_type": {
                "type": "string",
                "description": "Normally one of the allow-listed image or video MIME types.\nWhen the top-level `non_media` flag is true, this allow-list is\nbypassed and any content type is accepted (the size cap still\napplies).\n",
                "enum": [
                  "image/jpeg",
                  "image/png",
                  "image/gif",
                  "image/webp",
                  "image/heic",
                  "image/heif",
                  "video/mp4",
                  "video/webm",
                  "video/quicktime",
                  "video/x-msvideo"
                ]
              },
              "byte_size": {
                "type": "integer",
                "description": "Raw byte length. Limits: ≤ 20 MB for photos, ≤ 50 MB for\nvideos and (when `non_media` is true) any other content type.\n",
                "example": 2097152
              },
              "checksum": {
                "type": "string",
                "description": "Base64-encoded MD5 of the raw bytes (Active Storage convention).",
                "example": "q2QmVgL5xK+oR2VBoXcb9Q=="
              }
            }
          },
          "non_media": {
            "type": "boolean",
            "default": false,
            "description": "Optional. When true, the `blob.content_type` allow-list is bypassed\nso any file type (e.g. a PDF report or document) can be uploaded.\nWhen absent or false, only the allow-listed image/video types are\naccepted. The size cap always applies (photo types ≤ 20 MB,\neverything else ≤ 50 MB).\n",
            "example": false
          }
        },
        "required": [
          "blob"
        ]
      },
      "DirectUploadBlobResponse": {
        "type": "object",
        "description": "Signed URL the client PUTs raw bytes to, plus the `signed_id`\nused later to attach the blob to an inspection item.\n",
        "properties": {
          "signed_id": {
            "type": "string",
            "example": "eyJfcmFpbHMi..."
          },
          "direct_upload_url": {
            "type": "string",
            "example": "https://example-bucket.s3.amazonaws.com/abc?X-Amz-..."
          },
          "direct_upload_headers": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "HTTP headers the client must include on the PUT."
          },
          "filename": {
            "type": "string"
          },
          "content_type": {
            "type": "string"
          },
          "byte_size": {
            "type": "integer"
          },
          "checksum": {
            "type": "string"
          }
        },
        "required": [
          "signed_id",
          "direct_upload_url",
          "direct_upload_headers",
          "filename",
          "content_type",
          "byte_size"
        ]
      },
      "InspectionScheduleSummary": {
        "type": "object",
        "description": "Source: `Api::V1::Inspections::ScheduleSerializer`.\n",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "frequency": {
            "type": "string",
            "nullable": true
          },
          "day_of_week": {
            "type": "integer",
            "nullable": true
          },
          "day_of_month": {
            "type": "integer",
            "nullable": true
          },
          "active": {
            "type": "boolean"
          },
          "start_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "end_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "last_generated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "next_generation_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "ready_to_generate": {
            "type": "boolean"
          },
          "template": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "category": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "location": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "assigned_inspector": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "InspectionCycleItem": {
        "type": "object",
        "description": "A claimable cycle item on the \"Available to claim\" feed. An inspection\nonly exists AFTER a claim, so the claim pool serializes the\npre-inspection `InspectionCycleItem`. Source:\n`Api::V1::Inspections::CycleItemSerializer`.\n",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Cycle item id.",
            "example": 4
          },
          "name": {
            "type": "string",
            "nullable": true,
            "description": "The thing being inspected.",
            "example": "Forklift #3"
          },
          "cycle_id": {
            "type": "integer",
            "example": 7
          },
          "cycle_name": {
            "type": "string",
            "nullable": true,
            "example": "Q3 Safety Sweep"
          },
          "template_id": {
            "type": "integer",
            "nullable": true,
            "example": 81
          },
          "template_name": {
            "type": "string",
            "nullable": true,
            "example": "Daily Workplace Safety Inspection"
          },
          "location_id": {
            "type": "integer",
            "nullable": true,
            "example": 2
          },
          "location_name": {
            "type": "string",
            "nullable": true,
            "example": "Northeast Region"
          },
          "due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Prospective deadline — the cycle's end-of-day (the `due_at` stamped on the inspection at claim time). Null when the cycle has no end date."
          },
          "status": {
            "type": "string",
            "example": "pending"
          },
          "can_claim": {
            "type": "boolean",
            "description": "Server-authoritative. Always true in this feed (already filtered to the caller's claimable items).",
            "example": true
          },
          "claimed_by": {
            "type": "object",
            "nullable": true,
            "description": "The assigned inspector once claimed; null while in the pool.",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          }
        },
        "required": [
          "id",
          "cycle_id",
          "status",
          "can_claim"
        ]
      },
      "InspectionCycleSummary": {
        "type": "object",
        "description": "Source: `Api::V1::Inspections::CycleSerializer`.\n",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "cycle_type": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string"
          },
          "start_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "end_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "days_remaining": {
            "type": "integer",
            "nullable": true
          },
          "progress_percentage": {
            "type": "number",
            "format": "float",
            "nullable": true
          },
          "close_behavior": {
            "type": "string",
            "nullable": true
          },
          "template": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "my_inspection": {
            "type": "object",
            "nullable": true,
            "description": "The caller's in-progress or draft inspection for this cycle, if one exists. Lets the client show \"Resume\" instead of \"Start\". Null when the caller has no active inspection in the cycle.",
            "properties": {
              "id": {
                "type": "integer"
              },
              "status": {
                "type": "string"
              }
            }
          }
        }
      },
      "InspectionSweepSummary": {
        "type": "object",
        "description": "Source: `Api::V1::Inspections::SweepSummarySerializer`. A coverage\nsweep the current inspector can work (one per facility layout).\n",
        "properties": {
          "id": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "description": "in_progress | pending_approval | signed_off"
          },
          "sweep_assignment": {
            "type": "string",
            "description": "claim_pool | zones"
          },
          "location": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "facility_layout": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "coverage_percent": {
            "type": "integer",
            "nullable": true
          },
          "aisles_covered": {
            "type": "number",
            "format": "float",
            "nullable": true
          },
          "aisles_total": {
            "type": "integer",
            "nullable": true
          },
          "fully_covered": {
            "type": "boolean",
            "nullable": true
          },
          "last_session": {
            "type": "object",
            "nullable": true,
            "description": "Cross-person resume teaser; null until anyone has paused a session.",
            "properties": {
              "range_label": {
                "type": "string",
                "nullable": true
              },
              "frontier_bay": {
                "type": "integer",
                "nullable": true
              },
              "handoff_note": {
                "type": "string",
                "nullable": true
              },
              "paused_by": {
                "type": "string",
                "nullable": true
              },
              "paused_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              }
            }
          },
          "inspection_cycle": {
            "type": "object",
            "nullable": true,
            "description": "The parent cycle — its name is the \"inspection name\" the web shows (banner/breadcrumb/cockpit).",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "inspection_cycle_id": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "InspectionSweepCell": {
        "type": "object",
        "description": "One resolved (bay, level) cell. Only passed/exception cells exist; a (bay,level) not present is \"pending\".",
        "properties": {
          "bay": {
            "type": "integer"
          },
          "level": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "description": "passed | exception"
          }
        }
      },
      "InspectionSweepAisle": {
        "type": "object",
        "description": "Source: `Api::V1::Inspections::SweepAisleSerializer`. One aisle + its per-(bay,level) grid.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "label": {
            "type": "string"
          },
          "position": {
            "type": "integer"
          },
          "zone_name": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string",
            "description": "not_started | in_progress | covered | skipped"
          },
          "bays_total": {
            "type": "integer"
          },
          "depth": {
            "type": "integer"
          },
          "frontier_bay": {
            "type": "integer"
          },
          "skip_reason": {
            "type": "string",
            "nullable": true
          },
          "coverage_ratio": {
            "type": "number",
            "format": "float"
          },
          "bay_labels": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "level_elevations": {
            "type": "array",
            "items": {}
          },
          "current_inspector": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "assigned_inspector": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "cells": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InspectionSweepCell"
            }
          }
        }
      },
      "InspectionSweepException": {
        "type": "object",
        "description": "Source: `Api::V1::Inspections::SweepExceptionSerializer`. A coverage-sweep finding.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "aisle": {
            "type": "string",
            "nullable": true
          },
          "aisle_id": {
            "type": "integer",
            "nullable": true
          },
          "bay": {
            "type": "string",
            "nullable": true
          },
          "level": {
            "type": "string",
            "nullable": true
          },
          "component": {
            "type": "string",
            "nullable": true
          },
          "damage": {
            "type": "string",
            "nullable": true
          },
          "severity": {
            "type": "string",
            "description": "Observation | Minor | Major | Critical"
          },
          "status": {
            "type": "string",
            "description": "open | resolved | corrective | voided"
          },
          "note": {
            "type": "string",
            "nullable": true
          },
          "answers": {
            "type": "object"
          },
          "position_label": {
            "type": "string",
            "nullable": true
          },
          "inspector": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "photos": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "media_kind": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "captured_at": {
                  "type": "string",
                  "format": "date-time",
                  "nullable": true
                }
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "InspectionSweepDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/InspectionSweepSummary"
          },
          {
            "type": "object",
            "properties": {
              "can_write": {
                "type": "boolean"
              },
              "can_sign_off": {
                "type": "boolean"
              },
              "requires_approval": {
                "type": "boolean"
              },
              "aisles": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/InspectionSweepAisle"
                }
              },
              "exceptions": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/InspectionSweepException"
                }
              }
            }
          }
        ]
      },
      "InspectionSweepBundle": {
        "allOf": [
          {
            "$ref": "#/components/schemas/InspectionSweepDetail"
          },
          {
            "type": "object",
            "properties": {
              "rack_form_schema": {
                "type": "object",
                "description": "Component-branched log-issue form (RackFormSchema)."
              },
              "components": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "fetched_at": {
                "type": "string",
                "format": "date-time"
              },
              "etag": {
                "type": "string"
              }
            }
          }
        ]
      },
      "LeaderRoundsPagination": {
        "type": "object",
        "description": "Per-collection pagination. `/leader-rounds/my` returns three of these — one per collection — because it walks three independent cursors.",
        "required": [
          "page",
          "per_page",
          "total",
          "total_pages"
        ],
        "properties": {
          "page": {
            "type": "integer",
            "example": 1
          },
          "per_page": {
            "type": "integer",
            "example": 25
          },
          "total": {
            "type": "integer",
            "example": 42
          },
          "total_pages": {
            "type": "integer",
            "example": 2
          }
        }
      },
      "LeaderRoundsRoundSummary": {
        "type": "object",
        "description": "A round as it appears in a list. `private_notes` is present ONLY for the leader who led it — on any other tier the key is absent, not null.",
        "required": [
          "id",
          "subject_id",
          "subject_name",
          "leader_id",
          "leader_name",
          "occurred_on",
          "status"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 4
          },
          "subject_id": {
            "type": "integer",
            "example": 1752
          },
          "subject_name": {
            "type": "string",
            "example": "Marcus Bell"
          },
          "subject_job_title": {
            "type": "string",
            "nullable": true,
            "description": "The subject's credential, null when the tenant records none. How a person is identified on a floor (\"Marcus Bell, RN\") — two Bells on one unit is normal, so a bare name is ambiguous.",
            "example": "RN"
          },
          "subject_avatar_url": {
            "type": "string",
            "nullable": true,
            "description": "Absolute URL, null when no avatar is set."
          },
          "leader_id": {
            "type": "integer",
            "example": 893
          },
          "leader_name": {
            "type": "string",
            "example": "Priya Raman"
          },
          "leader_job_title": {
            "type": "string",
            "nullable": true,
            "description": "Emitted alongside the subject's because the two surfaces that render a round show different people: a leader's Recent Rounds shows the SUBJECT, Rounds About Me shows the LEADER.",
            "example": "Unit Leader"
          },
          "leader_avatar_url": {
            "type": "string",
            "nullable": true,
            "description": "Absolute URL, null when no avatar is set."
          },
          "template": {
            "type": "string",
            "description": "The template's NAME (not its id).",
            "example": "Staff Rounding"
          },
          "occurred_on": {
            "type": "string",
            "format": "date",
            "example": "2026-08-06"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "completed",
              "skipped"
            ],
            "example": "completed"
          },
          "skipped_reason": {
            "type": "string",
            "nullable": true,
            "description": "Present on a skipped round. A skip is a recorded decision the subject can read — \"subject on leave\" — not a gap in coverage."
          },
          "private_notes": {
            "type": "string",
            "nullable": true,
            "description": "Leader tier ONLY. Absent from the payload entirely for the subject and for anyone outside the leader's management chain — see `private_notes_visible` on the detail response."
          },
          "answers_count": {
            "type": "integer",
            "description": "Answers recorded on the round. Emitted by the LIST endpoints (where it is batched for the page) and by the create/detail replies."
          },
          "issues_count": {
            "type": "integer",
            "description": "Issues the round raised onto the stoplight ledger."
          },
          "open_issues_count": {
            "type": "integer",
            "description": "Of `issues_count`, how many are still open. LIST endpoints only. A round whose three issues are all resolved and one whose three are still open read identically from a total, and on a log of past rounds that difference is the point."
          }
        }
      },
      "LeaderRoundsTemplateQuestion": {
        "type": "object",
        "description": "One ordered, typed question. `field_type` is the platform form-field control to render; `question_type` is the key `answers` is submitted under. See the mapping table on GET /leader-rounds/templates.",
        "required": [
          "id",
          "position",
          "prompt",
          "question_type",
          "field_type",
          "required",
          "choices",
          "composite",
          "inputs"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "The key to use in the `answers` / `issues` hash on create.",
            "example": 5
          },
          "position": {
            "type": "integer",
            "example": 4
          },
          "prompt": {
            "type": "string",
            "example": "How supported do you feel in your role right now?"
          },
          "question_type": {
            "type": "string",
            "enum": [
              "text",
              "scale",
              "boolean",
              "choice",
              "recognition_pick",
              "issue_capture"
            ],
            "example": "scale"
          },
          "field_type": {
            "type": "string",
            "enum": [
              "textarea",
              "rating",
              "radio",
              "select",
              "lookup"
            ],
            "description": "The shared form-field control this question renders on.",
            "example": "rating"
          },
          "required": {
            "type": "boolean",
            "description": "A required question gates the save. These are the \"starred\" answers on the capture form.",
            "example": true
          },
          "choices": {
            "type": "array",
            "description": "Populated only for `choice`; an empty array otherwise, so a client never has to branch on nil-vs-empty.",
            "items": {
              "type": "string"
            },
            "example": []
          },
          "scale_min": {
            "type": "integer",
            "nullable": true,
            "description": "Present only for `scale`. Published because the bound is enforced in the model — a client that guessed 1..10 would build a picker whose top half always 422s.",
            "example": 1
          },
          "scale_max": {
            "type": "integer",
            "nullable": true,
            "example": 5
          },
          "composite": {
            "type": "boolean",
            "description": "True when this question needs MORE THAN ONE control. `field_type` names only the FIRST one, so a client that renders `field_type` alone silently omits the rest — which shipped: the native capture form drew a recognition_pick's person picker and not its citation, and because the recognition post body is built from that citation, every recognition posted from mobile carried no reason. A client never has to branch on this flag, because `inputs` is authoritative either way.",
            "example": true
          },
          "inputs": {
            "type": "array",
            "description": "THE RENDER LIST — always present and always complete: one entry for a simple question, several for a composite. Render every entry and a composite cannot be half-built.",
            "items": {
              "type": "object",
              "required": [
                "key",
                "submit_as",
                "field_type"
              ],
              "properties": {
                "key": {
                  "type": "string",
                  "description": "The submission key for this input, inside the question's own hash. A simple question's is `value`.",
                  "example": "referenced_user_id"
                },
                "submit_as": {
                  "type": "string",
                  "enum": [
                    "answers",
                    "issues"
                  ],
                  "description": "WHICH top-level hash this input posts under. Explicit because it is the part most easily got wrong: an `issue_capture`'s inputs go in `issues`, not `answers`, so a client that assumed one envelope for everything would drop the whole issue.",
                  "example": "answers"
                },
                "field_type": {
                  "type": "string",
                  "enum": [
                    "textarea",
                    "text",
                    "rating",
                    "radio",
                    "select",
                    "lookup",
                    "date"
                  ],
                  "example": "lookup"
                },
                "label": {
                  "type": "string",
                  "example": "Owner"
                },
                "prompt": {
                  "type": "string",
                  "description": "The parent question's prompt, repeated for convenience."
                },
                "placeholder": {
                  "type": "string",
                  "nullable": true
                },
                "help": {
                  "type": "string",
                  "nullable": true
                },
                "required": {
                  "type": "boolean",
                  "example": false
                },
                "lookup_source": {
                  "type": "string",
                  "nullable": true,
                  "description": "For `lookup` inputs — the record set to search.",
                  "example": "users"
                },
                "choices": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "example": [
                    "low",
                    "medium",
                    "high",
                    "critical"
                  ]
                },
                "default": {
                  "type": "string",
                  "nullable": true,
                  "description": "Server-side default when the input is left blank.",
                  "example": "medium"
                },
                "max_length": {
                  "type": "integer",
                  "nullable": true,
                  "description": "Published only where the server enforces it.",
                  "example": 2000
                }
              }
            }
          }
        }
      },
      "LeaderRoundsAnswer": {
        "type": "object",
        "description": "One answer on a round. `value` is the formatted display string; the typed columns ride alongside for clients that render the raw value (a scale drawn as filled pips rather than the text \"3\").",
        "required": [
          "id",
          "question_id",
          "prompt",
          "question_type",
          "field_type",
          "position"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "The ANSWER's id — what every write naming an answer takes, including `POST /leader-rounds/rounds/{id}/recognize`. DISTINCT from `question_id`, which identifies the template question this answers; sending that one instead is a lookup miss, not a match.",
            "example": 277
          },
          "question_id": {
            "type": "integer",
            "example": 5
          },
          "prompt": {
            "type": "string",
            "example": "How supported do you feel in your role right now?"
          },
          "question_type": {
            "type": "string",
            "enum": [
              "text",
              "scale",
              "boolean",
              "choice",
              "recognition_pick",
              "issue_capture"
            ]
          },
          "field_type": {
            "type": "string",
            "enum": [
              "textarea",
              "rating",
              "radio",
              "select",
              "lookup"
            ]
          },
          "required": {
            "type": "boolean"
          },
          "position": {
            "type": "integer"
          },
          "value": {
            "type": "string",
            "nullable": true,
            "description": "Formatted for display — \"Yes\"/\"No\" for boolean, the name for a recognition pick.",
            "example": "5"
          },
          "value_text": {
            "type": "string",
            "nullable": true
          },
          "value_number": {
            "type": "number",
            "nullable": true,
            "description": "The scale value."
          },
          "value_boolean": {
            "type": "boolean",
            "nullable": true
          },
          "referenced_user_id": {
            "type": "integer",
            "nullable": true,
            "description": "For a `recognition_pick` — the colleague named."
          },
          "referenced_user_name": {
            "type": "string",
            "nullable": true,
            "example": "Amy Okonkwo"
          },
          "recognition_posted": {
            "type": "boolean",
            "description": "True once the pick has been posted to the recipient's feed. Clients use this to show \"posted\" rather than offering the one-shot button again — a re-save must never spam the recipient.",
            "example": false
          }
        }
      },
      "LeaderRoundsIssue": {
        "type": "object",
        "description": "One row on the stoplight ledger — a `Capa::Action` sourced from a round. `stoplight` is DERIVED at read time, never stored.",
        "required": [
          "id",
          "description",
          "status",
          "stoplight"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 614
          },
          "description": {
            "type": "string",
            "example": "Vitals machine in bay 3 still out of service"
          },
          "status": {
            "type": "string",
            "description": "The status on the CAPA register.",
            "example": "in_progress"
          },
          "stoplight": {
            "type": "string",
            "enum": [
              "red",
              "yellow",
              "green"
            ],
            "description": "Derived: green = completed, red = cancelled OR open-past-due, yellow = the open remainder. A CANCELLED issue is red but is a decision — label it \"Won't fix\", not \"Red\".",
            "example": "yellow"
          },
          "due_date": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "example": "2026-08-31"
          },
          "assigned_to_id": {
            "type": "integer",
            "nullable": true
          },
          "assigned_to_name": {
            "type": "string",
            "nullable": true,
            "example": "Dana Whitfield"
          },
          "resolution_notes": {
            "type": "string",
            "nullable": true,
            "description": "Required when cancelling, and PUBLISHED to the person who raised the issue. This is the guardrail that keeps the ledger honest."
          },
          "round_id": {
            "type": "integer",
            "description": "The round this was raised in — tap-through target.",
            "example": 4
          },
          "pillar_id": {
            "type": "integer",
            "nullable": true,
            "description": "The strategic pillar this issue rolls up to. Resolved at read time: an explicit per-issue override wins, otherwise it is derived from the template question that raised the issue. `null` means Unaligned — either no override and an unmapped question, or an override deliberately pinning the issue to Unaligned. Settable via `PATCH /leader-rounds/issues/{id}`.",
            "example": 67
          },
          "pillar_name": {
            "type": "string",
            "nullable": true,
            "description": "Display name of `pillar_id`, resolved for the caller so a client does not need a second lookup. `null` whenever `pillar_id` is null.",
            "example": "People, Culture & Leadership"
          },
          "priority": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high",
              "critical"
            ],
            "nullable": true,
            "description": "As the leader set it on capture. Anything outside the enum is clamped to `medium` server-side, so a client never has to defend against a value it cannot render.",
            "example": "high"
          },
          "raised_by_name": {
            "type": "string",
            "nullable": true,
            "description": "The round's SUBJECT — the person who raised it. Deliberately not `created_by`, which RoundCreator sets to the round's LEADER unconditionally and would therefore name the same person on every row of that leader's own ledger. The web ledger links this same subject.",
            "example": "Tasha Green"
          },
          "created_by_name": {
            "type": "string",
            "nullable": true,
            "description": "WHO HANDED IT OVER — the round's leader, who created the issue. A DIFFERENT person from `raised_by_name` (the round's subject), and the one an assignee needs: nothing else in the payload says who gave them the work. Rendered as \"Assigned by\" on the subject screen.",
            "example": "Priya Raman"
          },
          "round_occurred_on": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "When the round took place, so a row can say WHEN something was raised rather than naming the raiser with no context.",
            "example": "2026-07-08"
          },
          "round_visible": {
            "type": "boolean",
            "description": "Whether the caller may OPEN the round this issue came from — a narrower question than whether they may read the issue, and not answerable from anything else in the payload. `GET /leader-rounds/issues/{id}` admits an issue merely ASSIGNED to the caller; `GET /leader-rounds/rounds/{id}` does not (an assignment grants no access to a round holding private notes and answers about a third party). A client must draw the link back to the round ONLY when this is true — otherwise it offers a route that always 404s. Emitted on the single-issue read AND on both issue writes (PATCH `/issues/{id}` and `/issues/{id}/status`), because a detail screen folds a write reply back into the record it is displaying — a reply that omitted it made the link to the round disappear the moment a status was saved. No LIST payload carries it (a query per row).",
            "example": true
          },
          "read_only": {
            "type": "boolean",
            "description": "Whether the CALLER may move this issue. COMPUTED per caller against the write tier (the rounds they lead or supervise) — being the ASSIGNEE grants nothing. A client draws a status control only when this is false; when it is true the absence is the point, and the surface should say who does own the status. Emitted on the single-issue read, on both issue writes (same fold-back reason as `round_visible`), and on the assigned-to-me rows of /my.",
            "example": false
          }
        }
      },
      "LeaderRoundsIssueHistory": {
        "type": "array",
        "description": "The CAPA action's audit trail: the notes IssueStatusUpdater writes atomically with each status change, plus any progress note posted through `POST /leader-rounds/issues/{id}/comments`. So the dates are REAL transitions rather than a summary composed afterwards.\nAN ENVELOPE KEY, not a property of `issue` — carried by the single-issue read and by the status write, and by nothing else. The list endpoints omit it because it is a query per row, which is why a detail screen has to fetch the record rather than render the row it was handed. Top-level notes only (replies are nested under their parent and not returned here) and soft-deleted notes are excluded.",
        "items": {
          "type": "object",
          "properties": {
            "id": {
              "type": "integer"
            },
            "author": {
              "type": "string",
              "nullable": true,
              "description": "Full name, or null if the author's user record is gone."
            },
            "author_avatar_url": {
              "type": "string",
              "nullable": true,
              "description": "Absolute URL, or null when the author has no photo (or no user record). `POST /leader-rounds/issues/{id}/comments` returns a `comment` in this SAME shape, so a client can append its reply straight into the trail rather than re-fetching."
            },
            "body": {
              "type": "string",
              "example": "Status changed: In progress → Completed — Replaced from spare pool."
            },
            "created_at": {
              "type": "string",
              "format": "date-time"
            }
          }
        }
      },
      "LeaderRoundsScopeMetric": {
        "type": "object",
        "description": "One labelled number on a scope-tree node. DELIBERATELY GENERIC — the client component that renders the picker must not need to know what is being counted, so it can be reused by the next app that grows a drill-down. Labels are server-authored and already localized.",
        "required": [
          "label",
          "value",
          "tone"
        ],
        "properties": {
          "label": {
            "type": "string",
            "example": "Red"
          },
          "value": {
            "type": "string",
            "description": "Pre-formatted for display, so the client does not re-format.",
            "example": "3"
          },
          "tone": {
            "type": "string",
            "enum": [
              "neutral",
              "warning",
              "danger",
              "success"
            ],
            "description": "Drives emphasis only; never the sole carrier of meaning.",
            "example": "danger"
          }
        }
      },
      "LeaderRoundsScopeNode": {
        "type": "object",
        "description": "One row in the scope picker — a leader and their subtree's numbers.",
        "required": [
          "id",
          "name",
          "leaf",
          "metrics"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 1222
          },
          "name": {
            "type": "string",
            "example": "Priya Raman"
          },
          "leaf": {
            "type": "boolean",
            "description": "No layer below. A leaf row APPLIES immediately rather than drilling into an empty level.",
            "example": false
          },
          "metrics": {
            "type": "array",
            "description": "Aggregated over the node's WHOLE subtree, matching the rollup's unit rows.",
            "items": {
              "$ref": "#/components/schemas/LeaderRoundsScopeMetric"
            }
          },
          "quiet": {
            "type": "boolean",
            "description": "Nothing anywhere in this node's subtree. These rows are ordered LAST (never dropped — the picker is the only way to reach a node, and on the rollup a team that raises nothing is often the coverage gap worth opening), so a client may collapse them behind a \"show all\" that states how many it is hiding.",
            "example": false
          }
        }
      },
      "NotepadErrorBody": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "NotepadValidationErrorBody": {
        "type": "object",
        "properties": {
          "errors": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "field": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "NotepadCursorMeta": {
        "type": "object",
        "description": "Keyset pagination. `next_cursor` is null on the last page.",
        "properties": {
          "limit": {
            "type": "integer"
          },
          "has_more": {
            "type": "boolean"
          },
          "next_cursor": {
            "type": "integer",
            "nullable": true
          }
        }
      },
      "NotepadListMeta": {
        "allOf": [
          {
            "$ref": "#/components/schemas/NotepadCursorMeta"
          },
          {
            "type": "object",
            "description": "The meetings list's `meta`. ONE key set across both paging modes, with an explicit null where a key does not apply — so a client can declare a single type for the endpoint instead of treating every key as optional.",
            "properties": {
              "page": {
                "type": "integer",
                "nullable": true,
                "description": "Null in keyset mode, which has no page number."
              },
              "filters": {
                "type": "object",
                "description": "What the page was actually computed under. Values are the APPLIED ones, not the raw params: an omitted narrowing echoes null rather than claiming something that never happened. The cursor is a bare meeting id carrying no filter identity, so this is how a client detects a stale cursor under changed filters.",
                "properties": {
                  "view": {
                    "type": "string",
                    "nullable": true,
                    "description": "`trash` when Recently Deleted was asked for; null otherwise."
                  },
                  "sort": {
                    "type": "string",
                    "nullable": true,
                    "description": "One of `edited` / `created` / `title`; null in keyset mode."
                  },
                  "q": {
                    "type": "string",
                    "nullable": true
                  },
                  "status": {
                    "type": "string",
                    "nullable": true
                  },
                  "filter": {
                    "type": "string",
                    "nullable": true
                  },
                  "notebook_id": {
                    "type": "integer",
                    "nullable": true
                  }
                }
              },
              "pagination": {
                "type": "object",
                "description": "Which paging mode answered, and what it cost the caller.",
                "properties": {
                  "mode": {
                    "type": "string",
                    "enum": [
                      "cursor",
                      "page"
                    ],
                    "description": "`page` when `sort` was sent, `cursor` otherwise. The two are exclusive: a keyset cursor over a non-id ordering skips and duplicates rows."
                  },
                  "ignored": {
                    "type": "array",
                    "description": "The paging params the caller SENT that this mode discarded. Empty when the request named one mode cleanly.",
                    "items": {
                      "type": "string",
                      "enum": [
                        "cursor",
                        "page"
                      ]
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "NotepadSectionName": {
        "type": "string",
        "description": "The SERVER section vocabulary. Note `insights`, not the client's `key_insights` tab name.",
        "enum": [
          "summary",
          "action_items",
          "insights"
        ]
      },
      "NotepadUserCompact": {
        "type": "object",
        "description": "Enough identity to render an avatar and a name.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "initials": {
            "type": "string"
          },
          "color": {
            "type": "string",
            "description": "Deterministic avatar colour."
          },
          "avatar_url": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "NotepadAssignee": {
        "type": "object",
        "description": "Like NotepadUserCompact but keyed `user_id` — this is an assignment, not a user record.",
        "properties": {
          "user_id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "initials": {
            "type": "string"
          },
          "color": {
            "type": "string"
          },
          "avatar_url": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "NotepadNotebookRef": {
        "type": "object",
        "nullable": true,
        "description": "Compact notebook reference embedded in a meeting. `name` and `title` are the SAME value — the notebook's `title` column — emitted twice on purpose so this embed and the `/notebooks` list overlap and a client can declare one Notebook type across both.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "title": {
            "type": "string",
            "description": "Alias of `name`; always identical."
          }
        }
      },
      "NotepadNotebook": {
        "type": "object",
        "description": "The `/notebooks` list row. Carries `name` alongside `title` for the same reason NotepadNotebookRef carries `title` — same value, both keys, so the two shapes of one entity overlap.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "description": "Alias of `title`; always identical."
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "note_count": {
            "type": "integer"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "NotepadMeetingRef": {
        "type": "object",
        "description": "Lightweight meeting reference for deep-linking.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "processing",
              "completed",
              "failed"
            ]
          },
          "date_label": {
            "type": "string"
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "NotepadMeetingSummary": {
        "type": "object",
        "description": "The list row. Carries denormalized counts and `artifacts_status` so a list render needs no per-row fan-out.",
        "properties": {
          "detail": {
            "type": "boolean",
            "description": "THE SHAPE DISCRIMINATOR, always present, so a typed client branches on it instead of probing for a key. `false` on this shape: the detail-only keys of NotepadMeetingDetail are ABSENT rather than null."
          },
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "processing",
              "completed",
              "failed"
            ],
            "description": "Client vocabulary — see the file header for the mapping."
          },
          "source": {
            "type": "string",
            "enum": [
              "system_audio",
              "voice_note",
              "upload",
              "paste",
              "quick_note"
            ],
            "description": "Client vocabulary — see the file header for the mapping."
          },
          "notebook_id": {
            "type": "integer",
            "nullable": true
          },
          "notebook": {
            "$ref": "#/components/schemas/NotepadNotebookRef"
          },
          "account": {
            "type": "object",
            "nullable": true,
            "description": "The Mango CS account the note is linked to, or null (ISS-20260910-524-C3B792). Set with `POST /ai_notepad/meetings/{id}/account`, cleared with the DELETE twin; pick from `GET /ai_notepad/meetings/linkable_accounts`.",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "date_label": {
            "type": "string",
            "description": "Server-rendered human date, so surfaces cannot disagree."
          },
          "duration_sec": {
            "type": "integer",
            "nullable": true
          },
          "excerpt": {
            "type": "string",
            "nullable": true,
            "description": "Short plain-text preview for the row, computed the same way the web list computes it (Apps::AiNotepadHelper#ai_notepad_note_excerpt): the opening block dropped when it merely repeats the title, tags stripped, truncated on a word boundary, and \"Processing…\" while the AI pipeline runs. Empty string when the note has no body."
          },
          "has_ai_lifecycle": {
            "type": "boolean",
            "description": "False for a plain typed note — it has no processing state to show."
          },
          "pinned": {
            "type": "boolean"
          },
          "pinned_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "deleted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "action_items_open_count": {
            "type": "integer"
          },
          "action_items_total_count": {
            "type": "integer"
          },
          "artifacts_status": {
            "type": "string",
            "enum": [
              "pending",
              "ready",
              "failed"
            ]
          },
          "created_by": {
            "allOf": [
              {
                "$ref": "#/components/schemas/NotepadUserCompact"
              }
            ],
            "nullable": true
          }
        }
      },
      "NotepadMeeting": {
        "allOf": [
          {
            "$ref": "#/components/schemas/NotepadMeetingSummary"
          }
        ],
        "description": "The shape write endpoints echo back. Same fields as the list row."
      },
      "NotepadMeetingEntity": {
        "allOf": [
          {
            "$ref": "#/components/schemas/NotepadMeetingSummary"
          },
          {
            "type": "object",
            "description": "The SERIALIZER's full meeting shape — the list row plus the detail-only keys. Signed/expiring media URLs and the share-link state are NOT here: the serializer stays request-agnostic and the detail controller merges those on top (see NotepadMeetingDetail).",
            "properties": {
              "detail": {
                "type": "boolean",
                "description": "Always `true` on this shape. See NotepadMeetingSummary."
              },
              "language": {
                "type": "string",
                "nullable": true
              },
              "error_message": {
                "type": "string",
                "nullable": true,
                "description": "Present only when `status` is `failed`."
              },
              "notes": {
                "type": "string",
                "nullable": true,
                "description": "Server-derived markdown twin of `notes_html`. Null for a recording-backed meeting."
              },
              "notes_html": {
                "type": "string",
                "nullable": true,
                "description": "The canonical rich-text body."
              },
              "can_edit": {
                "type": "boolean",
                "description": "Whether THIS caller may edit the content (owner/editor), so the client can gate its edit affordances instead of showing an Edit button that 403s."
              },
              "can_manage_sharing": {
                "type": "boolean",
                "description": "Whether this caller may open the Share dialog. A DIFFERENT right from `can_edit` — the members endpoints 403 a viewer — and the same predicate the web view gates on, so a client never offers a door the server refuses."
              },
              "summary_available": {
                "type": "boolean"
              },
              "insights_available": {
                "type": "boolean"
              },
              "has_audio_overview": {
                "type": "boolean"
              },
              "has_recording": {
                "type": "boolean"
              },
              "has_transcript": {
                "type": "boolean"
              },
              "speakers": {
                "type": "array",
                "description": "The speaker roster. Every element carries an `id` (`s1`, `s2`, …) that a transcript segment's `speaker` resolves against — derived from the element's position when the stored roster has none.",
                "items": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          }
        ]
      },
      "NotepadMeetingDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/NotepadMeetingEntity"
          },
          {
            "type": "object",
            "description": "What `GET /meetings/{id}` answers: the serializer's entity plus the request-specific affordances it deliberately leaves out.",
            "properties": {
              "recording": {
                "type": "object",
                "nullable": true,
                "description": "Signed, expiring — refetch rather than storing it.",
                "properties": {
                  "url": {
                    "type": "string",
                    "nullable": true
                  },
                  "expires_in": {
                    "type": "integer",
                    "nullable": true
                  },
                  "content_type": {
                    "type": "string",
                    "nullable": true
                  },
                  "byte_size": {
                    "type": "integer",
                    "nullable": true
                  }
                }
              },
              "audio_overview": {
                "$ref": "#/components/schemas/NotepadAudioOverview"
              },
              "document": {
                "type": "object",
                "nullable": true,
                "description": "The source document, for opening externally.",
                "properties": {
                  "web_url": {
                    "type": "string",
                    "nullable": true
                  },
                  "updated_at": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true
                  }
                }
              },
              "share_link": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/NotepadShareLink"
                  }
                ],
                "nullable": true,
                "description": "Always present; null until a public link exists. Public share links are not yet implemented, so this is currently always null."
              },
              "meeting": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/NotepadMeetingEntity"
                  }
                ],
                "description": "The identical serialized entity, repeated under the key every OTHER meeting-returning action uses (`{ meeting: … }`), so one \"render a meeting\" client path works against all of them. The bare root keys are the shipped contract and stay; this is the forward one. The request-specific extras above (`recording`, `audio_overview`, `document`, `share_link`) are deliberately NOT folded into it."
              }
            }
          }
        ]
      },
      "NotepadMutationAck": {
        "type": "object",
        "description": "ONE MERGEABLE SHAPE FOR THE FOUR TERSE ACKS — delete, restore, pin and unpin. Every one of them answers all five keys, so a client merges any of them into its cached row with a single code path and re-renders correctly. (This replaces the old NotepadPinState, which documented only `id`/`pinned`/`pinned_at`, and the delete/restore shapes, which documented only `id`/`deleted_at` and the wrong entity respectively.)\nBoth pairs matter on every ack: `pinned_at` is the column every viewer's list is ordered by, so a restore that did not report it left the client unable to place the note it just brought back; and `deleted`/`deleted_at` on a pin ack is what tells a client the row it is re-sorting is still live.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "deleted": {
            "type": "boolean"
          },
          "deleted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Non-null only while the note is in Recently Deleted; the client counts the 30-day purge window down from it."
          },
          "pinned": {
            "type": "boolean"
          },
          "pinned_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "NotepadCreateResponse": {
        "type": "object",
        "description": "ONE BODY SHAPE FOR EVERY OUTCOME OF the create. The action has three exits — the 201 that made a note, and the two 200s that hand back a note that already existed for the same calendar occurrence — and all three answer this, so a client merges any of them into one cache.",
        "properties": {
          "meeting": {
            "$ref": "#/components/schemas/NotepadMeeting"
          },
          "processing": {
            "type": "object",
            "description": "Whether the AI pipeline was actually enqueued, so a client can stop rendering a progress state for a job that was never started. `awaiting_media` is the OPPOSITE of `nothing_to_process` — an upload meeting is created with no media on purpose and the recording enqueues the job on its own request, so the progress affordance should stay up.",
            "properties": {
              "started": {
                "type": "boolean",
                "description": "True only when `reason` is `queued`."
              },
              "reason": {
                "type": "string",
                "enum": [
                  "queued",
                  "ai_disabled",
                  "auto_process_disabled",
                  "nothing_to_process",
                  "awaiting_media",
                  "already_exists"
                ]
              }
            }
          },
          "truncated_fields": {
            "type": "array",
            "description": "Which body fields the server's length ceiling cut. Always an array so the key set is stable; empty is the normal case.",
            "items": {
              "type": "string"
            }
          },
          "calendar": {
            "type": "object",
            "nullable": true,
            "description": "The calendar identity of the note being handed back, or null when it carries none. The dedup lookup deliberately keys on the event id and NOT the provider, so a create for one provider can legitimately return the note stored under another — `provider_mismatch` is how the client learns that rather than mislabelling the note.",
            "properties": {
              "event_id": {
                "type": "string"
              },
              "provider": {
                "type": "string",
                "nullable": true
              },
              "requested_provider": {
                "type": "string",
                "nullable": true
              },
              "provider_mismatch": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "NotepadArtifacts": {
        "type": "object",
        "description": "The AI output for a meeting.",
        "properties": {
          "meeting_id": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "ready",
              "failed"
            ],
            "description": "The ARTIFACTS vocabulary — `pending` means show a skeleton, not an error. Note this key carries the MEETING vocabulary on the sibling transcript and action-items reads, and the two COLLIDE on `failed`; `artifacts_status` below is the unambiguous name."
          },
          "artifacts_status": {
            "type": "string",
            "enum": [
              "pending",
              "ready",
              "failed"
            ],
            "description": "The same value as `status`, restated under the name all three of this meeting's reads share."
          },
          "meeting_status": {
            "type": "string",
            "enum": [
              "processing",
              "completed",
              "failed"
            ],
            "description": "The meeting's own lifecycle, unambiguously named."
          },
          "summary": {
            "type": "string",
            "nullable": true
          },
          "meeting_type": {
            "type": "string",
            "nullable": true
          },
          "sections": {
            "type": "array",
            "description": "Ordered insight sections, as rendered.",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string"
                },
                "label": {
                  "type": "string"
                },
                "items": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          },
          "decisions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "risks": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "themes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "error_message": {
            "type": "string",
            "nullable": true
          },
          "meta": {
            "type": "object",
            "description": "What the four collections above hold, and what was capped off them — the only read in the namespace that returned collections with no count and no bound.",
            "properties": {
              "sections_count": {
                "type": "integer",
                "description": "Sections BEFORE the cap, so a client sees what it is missing."
              },
              "items_count": {
                "type": "integer"
              },
              "decisions_count": {
                "type": "integer"
              },
              "risks_count": {
                "type": "integer"
              },
              "themes_count": {
                "type": "integer"
              },
              "max_sections": {
                "type": "integer"
              },
              "max_items": {
                "type": "integer"
              },
              "has_more": {
                "type": "boolean",
                "description": "True when any cap bit. There is no cursor — refetch is the only page."
              }
            }
          }
        }
      },
      "NotepadSectionSaveResponse": {
        "type": "object",
        "description": "Returned by every section write, so the client can re-render without a refetch.",
        "properties": {
          "meeting_id": {
            "type": "integer"
          },
          "section": {
            "$ref": "#/components/schemas/NotepadSectionName"
          },
          "artifacts": {
            "$ref": "#/components/schemas/NotepadArtifacts"
          },
          "action_items": {
            "type": "array",
            "description": "Present only for the `action_items` section.",
            "items": {
              "$ref": "#/components/schemas/NotepadActionItem"
            }
          },
          "saved_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "NotepadSectionVersion": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "section": {
            "$ref": "#/components/schemas/NotepadSectionName"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "author": {
            "$ref": "#/components/schemas/NotepadUserCompact"
          },
          "preview": {
            "type": "string",
            "nullable": true,
            "description": "Truncated content, for choosing a version to restore."
          }
        }
      },
      "NotepadTranscriptSegment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "seq": {
            "type": "integer",
            "description": "1-based ordinal; also the pagination cursor."
          },
          "t": {
            "type": "string",
            "nullable": true,
            "description": "Human time label."
          },
          "start_ms": {
            "type": "integer",
            "nullable": true,
            "description": "Currently always null — segments are derived from the stored transcript at request time, with no per-segment timing."
          },
          "end_ms": {
            "type": "integer",
            "nullable": true,
            "description": "Currently always null."
          },
          "speaker": {
            "type": "string",
            "nullable": true
          },
          "text": {
            "type": "string"
          },
          "is_final": {
            "type": "boolean"
          },
          "confidence": {
            "type": "number",
            "nullable": true,
            "description": "Currently always null."
          },
          "highlights": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          }
        }
      },
      "NotepadActionItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "meeting_id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "done": {
            "type": "boolean",
            "description": "The client view of `status`."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "completed"
            ]
          },
          "commitment_type": {
            "type": "string",
            "enum": [
              "hard",
              "soft"
            ]
          },
          "due_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "due_label": {
            "type": "string",
            "nullable": true,
            "description": "Server-rendered relative due label (\"Overdue\", \"Tomorrow\")."
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "assignee": {
            "allOf": [
              {
                "$ref": "#/components/schemas/NotepadAssignee"
              }
            ],
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "NotepadMember": {
        "type": "object",
        "description": "A person a meeting is shared with. The creator appears as an immutable synthetic `owner`.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "user_id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "nullable": true
          },
          "initials": {
            "type": "string"
          },
          "color": {
            "type": "string"
          },
          "avatar_url": {
            "type": "string",
            "nullable": true
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "editor",
              "viewer"
            ]
          },
          "is_owner": {
            "type": "boolean"
          },
          "shared_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "NotepadAudioCitation": {
        "type": "object",
        "description": "A citation on the narrated audio overview — it labels a SECTION of the narration script. A DIFFERENT entity from NotepadChatCitation, despite riding under the same `citations` field name; discriminate on the endpoint you called, never on which keys happen to exist. Every key is always present, with an explicit null when the value is missing.",
        "properties": {
          "index": {
            "type": "integer",
            "nullable": true,
            "description": "0-based section ordinal."
          },
          "label": {
            "type": "string",
            "nullable": true
          },
          "source_hint": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "NotepadAudioOverview": {
        "type": "object",
        "description": "Audio-overview state — the whole shape, identical wherever it appears (embedded on the meeting detail, and at the root of the dedicated read).",
        "properties": {
          "available": {
            "type": "boolean"
          },
          "failed": {
            "type": "boolean",
            "description": "The generation terminally FAILED. A third state the shape used to lack, which left a client that POSTed and then polled seeing `available: false` forever on a run that would never finish."
          },
          "state": {
            "type": "string",
            "enum": [
              "ready",
              "failed",
              "none"
            ],
            "description": "THE ONE STATE FIELD, so a poll has something to poll on — the same key and vocabulary the POST answers with (`generating` there). There is deliberately no `generating` value on a read: nothing on the meeting records that a run is in flight, and inventing one from the job queue would make a read depend on GoodJob's retention. A client polls `none` until it becomes `ready` or `failed`."
          },
          "error": {
            "type": "string",
            "nullable": true,
            "description": "The failure reason. Non-null only when `failed` is true."
          },
          "url": {
            "type": "string",
            "nullable": true,
            "description": "Signed and expiring — refetch rather than storing it."
          },
          "expires_in": {
            "type": "integer",
            "nullable": true,
            "description": "Seconds."
          },
          "script": {
            "type": "string",
            "nullable": true,
            "description": "The narration script, for captions drawn while the MP3 plays. Truncated to the same ceiling the synthesizer used, so the captions cannot outrun the audio."
          },
          "citations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/NotepadAudioCitation"
            }
          }
        }
      },
      "NotepadAudioOverviewDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/NotepadAudioOverview"
          },
          {
            "type": "object",
            "properties": {
              "meeting_id": {
                "type": "integer"
              },
              "audio_overview": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/NotepadAudioOverview"
                  }
                ],
                "description": "The identical object, repeated under the key `meetings#show` nests it at. ONE ENTITY, ONE ACCESSOR — this was the only entity read in the namespace served at the top level, so a client needed two accessors for one object depending on which endpoint it had called. The root keys are the shipped contract and stay."
              }
            }
          }
        ]
      },
      "NotepadChatCitation": {
        "type": "object",
        "description": "A citation on a grounded chat answer — it points at a SOURCE MEETING, not into the transcript. `index` is the same N the inline `[Source N]` marker in the answer text carries, renumbered so the first cited source is 1. Citations are grounded in retrieved content chunks rather than transcript segments, so there is no per-segment time label; `position` is the chunk's ordinal within its meeting. Every key is always present, with an explicit null when the value is missing.",
        "properties": {
          "index": {
            "type": "integer",
            "nullable": true
          },
          "meeting_id": {
            "type": "integer",
            "nullable": true
          },
          "meeting_title": {
            "type": "string",
            "nullable": true
          },
          "excerpt": {
            "type": "string",
            "nullable": true,
            "description": "Up to 200 characters."
          },
          "position": {
            "type": "integer",
            "nullable": true
          }
        }
      },
      "NotepadChatMessage": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "role": {
            "type": "string",
            "enum": [
              "user",
              "assistant"
            ]
          },
          "content": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "citations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/NotepadChatCitation"
            }
          }
        }
      },
      "NotepadUpcomingMeeting": {
        "type": "object",
        "description": "A calendar event starting soon, for the pre-meeting prompt. NOT a row of `GET /meetings` even though both live under a `meetings` key: the two share exactly one field name (`title`), this one keys the note as `meeting_id` where the list uses `id`, and everything else is calendar data the list has never carried. `kind` is the tag that tells them apart.",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "upcoming_meeting"
            ],
            "description": "The per-element discriminator against a meetings-list row."
          },
          "calendar_event_id": {
            "type": "string",
            "nullable": true
          },
          "calendar_provider": {
            "type": "string",
            "nullable": true,
            "description": "Send this and `calendar_event_id` back on `POST /meetings` so the note the desktop makes and the note the web makes for one occurrence are the same row."
          },
          "title": {
            "type": "string"
          },
          "starts_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "ends_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "starts_in_seconds": {
            "type": "integer"
          },
          "join_url": {
            "type": "string",
            "nullable": true
          },
          "location": {
            "type": "string",
            "nullable": true
          },
          "meeting_id": {
            "type": "integer",
            "nullable": true,
            "description": "The notepad note that already exists for this occurrence, or null — the client opens the existing note rather than starting a second one."
          }
        }
      },
      "NotepadShareLink": {
        "type": "object",
        "description": "A public, tokenised link to a read-only snapshot. NOT yet implemented — the field exists so clients bind against a stable shape, and is currently always null on the meeting detail.",
        "properties": {
          "token": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "scope": {
            "type": "string",
            "enum": [
              "summary",
              "full"
            ]
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "revoked_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "NotepadPreferences": {
        "type": "object",
        "description": "EFFECTIVE preferences — the user's stored values resolved against the tenant policy.",
        "properties": {
          "meeting_detection_enabled": {
            "type": "boolean"
          },
          "auto_capture": {
            "type": "string"
          },
          "default_notebook_id": {
            "type": "integer",
            "nullable": true
          },
          "default_language": {
            "type": "string",
            "nullable": true
          },
          "retain_audio_days": {
            "type": "integer"
          },
          "notify_on_ready": {
            "type": "boolean"
          },
          "default_template_id": {
            "type": "integer",
            "nullable": true
          },
          "capture_consent_granted": {
            "type": "boolean"
          },
          "consent_notice_required": {
            "type": "boolean"
          },
          "meeting_reminders_enabled": {
            "type": "boolean"
          },
          "meeting_reminder_lead_minutes": {
            "type": "integer"
          },
          "user_id": {
            "type": "string"
          },
          "business_id": {
            "type": "string"
          },
          "policy": {
            "type": "object",
            "description": "The admin-set ceiling these preferences resolve against.",
            "properties": {
              "capture_enabled": {
                "type": "boolean"
              },
              "max_retain_audio_days": {
                "type": "integer"
              },
              "consent_notice_text": {
                "type": "string",
                "nullable": true
              },
              "admin_owned": {
                "type": "array",
                "description": "Keys a user cannot override.",
                "items": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "AlertUser": {
        "type": "object",
        "description": "Basic sender details (canonical v1 user shape — id / name / avatar_url).",
        "nullable": true,
        "required": [
          "id"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "avatar_url": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "AlertSummary": {
        "type": "object",
        "description": "Alert shape returned by the list endpoint. `acknowledged` /\n`acknowledged_at` / `checkin_status` / `checked_in_at` / `response_pending`\nreflect the CALLER's own response state; `author` is the sender.\n",
        "required": [
          "id",
          "title",
          "body",
          "alert_type",
          "status",
          "urgent",
          "ack_required",
          "safety_check_in_required",
          "channels",
          "recipients_count",
          "author",
          "acknowledged",
          "response_pending",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "body": {
            "type": "string"
          },
          "alert_type": {
            "type": "string",
            "description": "Display type derived from the flags (response-first).",
            "enum": [
              "urgent",
              "acknowledge",
              "safety_check_in"
            ]
          },
          "status": {
            "type": "string",
            "example": "sent"
          },
          "urgent": {
            "type": "boolean"
          },
          "ack_required": {
            "type": "boolean"
          },
          "safety_check_in_required": {
            "type": "boolean"
          },
          "channels": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "recipients_count": {
            "type": "integer"
          },
          "author": {
            "$ref": "#/components/schemas/AlertUser"
          },
          "acknowledged": {
            "type": "boolean",
            "description": "Whether the calling user has acknowledged this alert."
          },
          "acknowledged_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "checkin_status": {
            "type": "string",
            "nullable": true,
            "enum": [
              "safe",
              "needs_help",
              null
            ],
            "description": "The calling user's safety check-in status, if any."
          },
          "checked_in_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "checkin_message": {
            "type": "string",
            "nullable": true,
            "description": "The message the caller entered in the check-in flow (the `needs_help`\nnote; null when they marked safe or haven't checked in). Returned by\nGET /alerts/{id} only.\n"
          },
          "response_pending": {
            "type": "boolean",
            "description": "True when the alert needs a response the caller hasn't given yet."
          },
          "sent_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "approval_id": {
            "type": "integer",
            "nullable": true,
            "description": "The Comms Hub approval request id. Present on `pending_approvals`\nitems (pass it to POST /approvals/{id}/approve|reject), on\n`?filter=draft` items that have a request (the latest one — null for a\nplain draft), and on GET /alerts/{id}. Omitted from the other `alerts`\nfilters.\n"
          },
          "approval_state": {
            "type": "string",
            "nullable": true,
            "enum": [
              "draft",
              "pending_approval",
              "approved",
              "rejected"
            ],
            "description": "The alert's position in the approval pipeline, from its latest Comms\nHub approval request: `draft` (never submitted / withdrawn),\n`pending_approval` (awaiting a reviewer), `approved` (cleared, pending\nthe author's send), or `rejected`. Present on `?filter=draft` items\nand on GET /alerts/{id} for a DRAFT alert. On the detail view it is\n`null` once the alert is published, for a draft that never entered the\napproval pipeline, and for a caller who may not see the alert's\ninternal review trail (non-author, non-privileged).\n"
          },
          "approval_decided_by": {
            "type": "string",
            "nullable": true,
            "description": "The NAME of the reviewer who approved or rejected the alert. On\n`?filter=draft` items and GET /alerts/{id}. Populated only when\n`approval_state` is `approved` or `rejected`; null otherwise (and null\non the detail view for callers who can't see the review trail).\n"
          },
          "approval_decided_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the alert was approved/rejected (ISO-8601). On `?filter=draft`\nitems and GET /alerts/{id}. Null unless `approval_state` is\n`approved`/`rejected`.\n"
          },
          "approval_notes": {
            "type": "string",
            "nullable": true,
            "description": "The reviewer's decision notes for the approval/rejection. On\n`?filter=draft` items and GET /alerts/{id}. Null unless\n`approval_state` is `approved`/`rejected` (and null when the reviewer\nleft no note, or for a detail-view caller who can't see the trail).\n"
          }
        }
      },
      "AlertInput": {
        "type": "object",
        "description": "Writable alert attributes (create).",
        "required": [
          "title",
          "body"
        ],
        "properties": {
          "title": {
            "type": "string"
          },
          "body": {
            "type": "string"
          },
          "sms_body": {
            "type": "string"
          },
          "action_url": {
            "type": "string"
          },
          "urgent": {
            "type": "boolean"
          },
          "ack_required": {
            "type": "boolean"
          },
          "safety_check_in_required": {
            "type": "boolean"
          },
          "audience_id": {
            "type": "integer"
          },
          "channels": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "AlertTemplate": {
        "type": "object",
        "description": "A reusable alert composition offered in the composer's template picker.\n`your_templates` items are this business's saved templates;\n`common_scenarios` items are platform-curated system templates\n(`system_template: true`). The composition fields pre-fill the new-alert\nform; the saved default audience lives in `metadata` (empty for system\nscenarios).\n",
        "required": [
          "id",
          "name",
          "system_template",
          "body",
          "channels"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "category": {
            "type": "string",
            "nullable": true
          },
          "system_template": {
            "type": "boolean",
            "description": "True for platform-curated common scenarios (business_id is null)."
          },
          "title": {
            "type": "string",
            "nullable": true
          },
          "body": {
            "type": "string"
          },
          "sms_body": {
            "type": "string",
            "nullable": true
          },
          "channels": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "urgent": {
            "type": "boolean"
          },
          "ack_required": {
            "type": "boolean"
          },
          "safety_check_in_required": {
            "type": "boolean"
          },
          "metadata": {
            "type": "object",
            "description": "Raw targeting blob the composer hydrates the new-alert form from —\nholds `extra_user_ids`, `audience_criteria`, and\n`recipient_group_ids`. Empty object for system scenarios.\n",
            "additionalProperties": true
          },
          "audience": {
            "type": "object",
            "nullable": true,
            "description": "Resolved delivery-target (\"Audience\") breakdown — present on the show\nendpoint and on `pending_approvals` / `?filter=draft` items (omitted\nfrom the paginated received list to stay N+1-free). Every selected\nentity is resolved to `{ id, name }` so the client renders\nhuman-readable labels instead of bare ids.\n",
            "properties": {
              "id": {
                "type": "integer",
                "nullable": true,
                "description": "Bound CommsHub audience id (null when none)."
              },
              "name": {
                "type": "string",
                "nullable": true
              },
              "summary": {
                "type": "string",
                "nullable": true
              },
              "recipient_groups": {
                "type": "array",
                "description": "Selected notification recipient groups, each as { id, name }.",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "name": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              },
              "specific_user_ids": {
                "type": "array",
                "description": "Ad-hoc specific recipients, each resolved to { id, name }.",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "name": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              },
              "audience_criteria": {
                "type": "array",
                "description": "Attribute-based targeting, one entry per criterion. Each carries\nits `type` and an `items` array of `{ id, name }`: id-based types\n(`location`, `department`) resolve to the record's name; value-based\ntypes (`role`, `job_title`) echo the value as both id and name;\n`everyone` (and any unknown type) carries an empty `items`.\n",
                "items": {
                  "type": "object",
                  "properties": {
                    "type": {
                      "type": "string",
                      "enum": [
                        "location",
                        "department",
                        "role",
                        "job_title",
                        "everyone"
                      ]
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "oneOf": [
                              {
                                "type": "integer"
                              },
                              {
                                "type": "string"
                              }
                            ]
                          },
                          "name": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "AlertListMeta": {
        "type": "object",
        "description": "Pagination + \"needs my response\" counts for the list endpoint, spanning\nthe whole received inbox (independent of the active `filter`):\n  * acknowledge     — ack-required alerts the caller has NOT acknowledged\n  * safety_check_in — safety-check-in alerts the caller has NOT responded to\n  * all             — the sum of the two (total responses the caller owes)\nNo `urgent` count — the accountability invariant makes every alert urgent,\nso it would just equal `all`.\n",
        "required": [
          "total_count",
          "total_pages",
          "current_page",
          "per_page",
          "segment_counts"
        ],
        "properties": {
          "total_count": {
            "type": "integer"
          },
          "total_pages": {
            "type": "integer"
          },
          "current_page": {
            "type": "integer"
          },
          "per_page": {
            "type": "integer"
          },
          "segment_counts": {
            "type": "object",
            "required": [
              "all",
              "acknowledge",
              "safety_check_in",
              "draft"
            ],
            "properties": {
              "all": {
                "type": "integer"
              },
              "acknowledge": {
                "type": "integer"
              },
              "safety_check_in": {
                "type": "integer"
              },
              "draft": {
                "type": "integer",
                "description": "The caller's OWN unsent alerts (authored + status 'draft') — the count for the ?filter=draft tab. Author-scoped, unlike the received-inbox counts above.\n"
              }
            }
          }
        }
      },
      "ApprovalUser": {
        "type": "object",
        "description": "Canonical v1 user shape (requester) — id / name / avatar_url.",
        "nullable": true,
        "required": [
          "id"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "avatar_url": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "Approval": {
        "type": "object",
        "description": "Approval context block — identical to the `approval` block on each\nsource's index `pending_approvals` item, and returned (post-decision) by\nthe decision endpoints. `can_act` is the caller's live ability to act at\nthe current step.\n",
        "required": [
          "request_id",
          "status",
          "requested_at",
          "current_step",
          "can_act"
        ],
        "properties": {
          "request_id": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "approved",
              "rejected",
              "withdrawn"
            ]
          },
          "requested_by": {
            "$ref": "#/components/schemas/ApprovalUser"
          },
          "requested_at": {
            "type": "string",
            "format": "date-time"
          },
          "current_step": {
            "type": "object",
            "required": [
              "index",
              "total"
            ],
            "properties": {
              "index": {
                "type": "integer",
                "description": "Zero-based index of the current step."
              },
              "total": {
                "type": "integer",
                "description": "Total number of steps in the workflow."
              },
              "role": {
                "type": "string",
                "nullable": true,
                "description": "The UserBusiness role that can act on this step."
              },
              "label": {
                "type": "string",
                "nullable": true,
                "description": "Human label for this step."
              }
            }
          },
          "can_act": {
            "type": "boolean"
          }
        }
      },
      "ApprovalDecision": {
        "type": "object",
        "description": "Response from POST /approvals/{id}/approve and /reject.",
        "required": [
          "source_type",
          "source_id",
          "approval"
        ],
        "properties": {
          "source_type": {
            "type": "string",
            "description": "The approved/rejected item's source type.",
            "enum": [
              "Broadcast",
              "Alert"
            ]
          },
          "source_id": {
            "type": "integer",
            "description": "The source record's id (broadcast or alert)."
          },
          "approval": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Approval"
              }
            ],
            "description": "The approval state AFTER the decision — `status` reflects the new\nstate (`approved` / `rejected`, or still `pending` if the workflow\nadvanced to a later step) and `can_act` is `false`.\n"
          }
        }
      },
      "MessageResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          },
          "conversation_id": {
            "type": "string",
            "format": "uuid",
            "description": "Unique identifier for this conversation",
            "example": "550e8400-e29b-41d4-a716-446655440000"
          },
          "status": {
            "type": "string",
            "enum": [
              "processing",
              "completed",
              "error"
            ],
            "example": "processing"
          },
          "websocket": {
            "type": "object",
            "description": "WebSocket subscription details",
            "properties": {
              "channel": {
                "type": "string",
                "example": "AiResponseChannel"
              },
              "subscription": {
                "type": "object",
                "properties": {
                  "conversation_id": {
                    "type": "string",
                    "format": "uuid"
                  }
                }
              }
            }
          }
        }
      },
      "Conversation": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Conversation session ID"
          },
          "type": {
            "type": "string",
            "enum": [
              "forms",
              "tasks",
              "epms",
              "general"
            ],
            "example": "general"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "completed",
              "archived"
            ],
            "example": "active"
          },
          "message_count": {
            "type": "integer",
            "example": 12
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "Message": {
        "type": "object",
        "properties": {
          "role": {
            "type": "string",
            "enum": [
              "user",
              "assistant",
              "system"
            ],
            "example": "assistant"
          },
          "content": {
            "type": "string",
            "example": "You have 15 PTO days remaining for this year."
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true,
            "description": "Additional message metadata (intent, tool usage, etc.)"
          }
        }
      },
      "Pagination": {
        "type": "object",
        "properties": {
          "total_count": {
            "type": "integer",
            "example": 25
          },
          "current_page": {
            "type": "integer",
            "example": 1
          },
          "per_page": {
            "type": "integer",
            "example": 20
          },
          "total_pages": {
            "type": "integer",
            "example": 2
          },
          "has_more": {
            "type": "boolean",
            "example": true
          }
        }
      },
      "AgentInfo": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "example": "Scheduling Assistant"
          },
          "slug": {
            "type": "string",
            "example": "scheduling"
          },
          "description": {
            "type": "string",
            "example": "Helps with shifts, schedules, and time off requests"
          },
          "icon": {
            "type": "string",
            "example": "calendar"
          },
          "examples": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "What shifts do I have this week?",
              "Request time off for next Friday",
              "Who is working tomorrow?"
            ]
          }
        }
      },
      "WebSocketEvent": {
        "type": "object",
        "description": "Events broadcast via WebSocket on AiResponseChannel.\nSubscribe with { channel: \"AiResponseChannel\", conversation_id: \"uuid\" }\n",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "chunk",
              "complete",
              "error",
              "status",
              "cancelled"
            ],
            "description": "- chunk: Streaming text fragment\n- complete: Full response with metadata\n- error: Error occurred\n- status: Status update (thinking, generating)\n- cancelled: Request was cancelled\n"
          },
          "content": {
            "type": "string",
            "description": "Text content (for chunk and complete types)"
          },
          "message": {
            "type": "string",
            "description": "Status or error message"
          },
          "metadata": {
            "type": "object",
            "description": "Additional data (for complete type)",
            "properties": {
              "conversation_id": {
                "type": "string"
              },
              "completed_at": {
                "type": "string",
                "format": "date-time"
              },
              "tokens_generated": {
                "type": "integer"
              }
            }
          }
        }
      },
      "AskAiSettings": {
        "type": "object",
        "description": "Feature settings for the mobile AI client, including current conversation for session restoration",
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          },
          "settings": {
            "type": "object",
            "properties": {
              "create_support_ticket_enabled": {
                "type": "boolean",
                "description": "Whether \"Create Support Ticket\" button should be shown",
                "example": true
              },
              "function_calls_enabled": {
                "type": "boolean",
                "description": "Whether AI can execute actions on behalf of the user",
                "example": true
              },
              "schedule_queries_enabled": {
                "type": "boolean",
                "description": "Whether scheduling-related queries are available",
                "example": true
              },
              "general_questions_enabled": {
                "type": "boolean",
                "description": "Whether general Q&A is enabled",
                "example": true
              },
              "clear_conversation_enabled": {
                "type": "boolean",
                "description": "Whether the admin allows users to clear their conversation. Check this\nbefore showing a \"Clear conversation\" / \"Reset AI\" control —\n`DELETE /ask_ai/conversations/{id}` returns 403\n`clear_conversation_disabled` when it is false.\n",
                "example": true
              },
              "voice_mode_enabled": {
                "type": "boolean",
                "description": "Whether voice mode is available for this business. Check this before showing voice button.",
                "example": true
              },
              "voice_mode_limit_info": {
                "type": "object",
                "nullable": true,
                "description": "Voice mode usage limits (only present if voice_mode_enabled is true)",
                "properties": {
                  "daily_used": {
                    "type": "integer",
                    "description": "Minutes used today",
                    "example": 5
                  },
                  "daily_limit": {
                    "type": "integer",
                    "description": "Daily limit in minutes",
                    "example": 30
                  },
                  "remaining": {
                    "type": "integer",
                    "description": "Remaining minutes for today",
                    "example": 25
                  },
                  "session_limit": {
                    "type": "integer",
                    "description": "Max minutes per session",
                    "example": 15
                  }
                }
              }
            }
          },
          "current_conversation": {
            "nullable": true,
            "description": "The user's current/latest active conversation for session restoration.\nReturns null if no active conversation exists.\nUse this to restore conversation history after logout/login.\n",
            "allOf": [
              {
                "$ref": "#/components/schemas/Conversation"
              }
            ]
          }
        }
      },
      "VoiceErrorResponse": {
        "type": "object",
        "description": "Error response for voice mode requests",
        "properties": {
          "success": {
            "type": "boolean",
            "example": false
          },
          "error": {
            "type": "string",
            "description": "Error message",
            "example": "Voice mode daily limit reached"
          },
          "limit_info": {
            "type": "object",
            "description": "Current usage limits (if applicable)",
            "properties": {
              "daily_used": {
                "type": "integer"
              },
              "daily_limit": {
                "type": "integer"
              },
              "remaining": {
                "type": "integer"
              }
            }
          },
          "billing_info": {
            "type": "object",
            "description": "Billing information (if applicable)",
            "properties": {
              "balance": {
                "type": "integer"
              },
              "resource_type": {
                "type": "string",
                "example": "voice_minutes"
              }
            }
          }
        }
      },
      "VoiceRealtimeChannelEvent": {
        "type": "object",
        "description": "Events broadcast via ActionCable on VoiceRealtimeChannel.\nSubscribe with { channel: \"VoiceRealtimeChannel\", session_id: \"uuid\" }\n\n**Sending Actions:**\n- `process_query`: Send transcript for backend processing\n- `end_session`: Gracefully end the voice session\n\n**Receiving Events:**\n- `query_response`: Backend AI response ready\n- `query_error`: Query processing failed\n- `session_ended`: Session finalized with billing info\n",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "query_response",
              "query_error",
              "session_ended"
            ],
            "description": "Event type"
          },
          "text": {
            "type": "string",
            "description": "AI response text (for query_response)"
          },
          "success": {
            "type": "boolean",
            "description": "Whether the query was successful"
          },
          "error": {
            "type": "string",
            "description": "Error message (for query_error)"
          },
          "metadata": {
            "type": "object",
            "description": "Additional response metadata",
            "properties": {
              "intent": {
                "type": "string"
              },
              "confidence": {
                "type": "number"
              }
            }
          },
          "duration_minutes": {
            "type": "number",
            "description": "Final session duration (for session_ended)"
          },
          "cost_cents": {
            "type": "integer",
            "description": "Final session cost (for session_ended)"
          },
          "billed": {
            "type": "boolean",
            "description": "Whether billing was processed (for session_ended)"
          }
        }
      },
      "BroadcastUser": {
        "type": "object",
        "description": "Basic sender details (canonical v1 user shape — id / name / avatar_url).",
        "nullable": true,
        "required": [
          "id"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "avatar_url": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "BroadcastSummary": {
        "type": "object",
        "description": "Broadcast shape returned by the list endpoint. `viewed` / `acknowledged`\nreflect the CALLER's own state for this broadcast; `created_by` is the\nsender.\n",
        "required": [
          "id",
          "title",
          "description",
          "status",
          "is_critical",
          "require_acknowledgment",
          "viewed",
          "acknowledged",
          "created_by",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "example": "published"
          },
          "is_critical": {
            "type": "boolean"
          },
          "require_acknowledgment": {
            "type": "boolean"
          },
          "scheduled_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When a \"send later\" broadcast will go out. Non-null only while `status` is `scheduled`."
          },
          "delivery_mode": {
            "type": "string",
            "enum": [
              "immediate",
              "on_shift_only",
              "next_shift_start"
            ],
            "description": "The shift-aware delivery window this broadcast was composed with. Always present; `immediate` when none was chosen."
          },
          "auto_widen_channels": {
            "type": "boolean",
            "description": "Whether each unacknowledged reminder widens the delivery channel (email -> +SMS -> +voice). Always present; `false` when off."
          },
          "publish_to_signage": {
            "type": "boolean",
            "description": "Whether this broadcast is marked for the break-room screen rotation. Always present; `false` when off. A marking, not a delivery receipt — each screen still shows it only if its own Communications content rule is on, and only while the broadcast is published."
          },
          "signage_location_ids": {
            "type": "array",
            "description": "Sites the screen marking is narrowed to. Empty means every screen (and is always empty when `publish_to_signage` is false).",
            "items": {
              "type": "integer"
            }
          },
          "viewed": {
            "type": "boolean",
            "description": "Whether the calling user has viewed this broadcast."
          },
          "acknowledged": {
            "type": "boolean",
            "description": "Whether the calling user has acknowledged this broadcast."
          },
          "created_by": {
            "$ref": "#/components/schemas/BroadcastUser"
          },
          "recipients_count": {
            "type": "integer",
            "description": "Projected recipient count (resolved audience). Present on\n`pending_approvals` items so an approver sees the reach; omitted from\nthe main `broadcasts` list.\n"
          },
          "approval_id": {
            "type": "integer",
            "description": "The Comms Hub approval request id — present ONLY on\n`pending_approvals` items; pass it to POST /approvals/{id}/approve|\nreject. Omitted from the main `broadcasts` list.\n"
          },
          "published_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "BroadcastDetail": {
        "type": "object",
        "description": "Full broadcast shape returned by show / create / update / publish.",
        "required": [
          "id",
          "title",
          "status",
          "is_critical",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "is_critical": {
            "type": "boolean"
          },
          "published_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "description": {
            "type": "string"
          },
          "require_acknowledgment": {
            "type": "boolean"
          },
          "scheduled_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When a \"send later\" broadcast will go out. Non-null only while `status` is `scheduled`."
          },
          "delivery_mode": {
            "type": "string",
            "enum": [
              "immediate",
              "on_shift_only",
              "next_shift_start"
            ],
            "description": "The shift-aware delivery window this broadcast was composed with. Always present; `immediate` when none was chosen."
          },
          "auto_widen_channels": {
            "type": "boolean",
            "description": "Whether each unacknowledged reminder widens the delivery channel (email -> +SMS -> +voice). Always present; `false` when off."
          },
          "publish_to_signage": {
            "type": "boolean",
            "description": "Whether this broadcast is marked for the break-room screen rotation. Always present; `false` when off. A marking, not a delivery receipt — each screen still shows it only if its own Communications content rule is on, and only while the broadcast is published."
          },
          "signage_location_ids": {
            "type": "array",
            "description": "Sites the screen marking is narrowed to. Empty means every screen (and is always empty when `publish_to_signage` is false).",
            "items": {
              "type": "integer"
            }
          },
          "viewed": {
            "type": "boolean",
            "description": "Whether the calling user has viewed this broadcast."
          },
          "acknowledged": {
            "type": "boolean",
            "description": "Whether the calling user has acknowledged this broadcast. On GET /broadcasts/{id} this is always true when the caller AUTHORED the broadcast and is not in its audience — there is nothing for the author to acknowledge; otherwise it reflects the caller's own acknowledgment (an author who IS a recipient must acknowledge like anyone else)."
          },
          "created_by": {
            "$ref": "#/components/schemas/BroadcastUser"
          },
          "can_manage": {
            "type": "boolean",
            "description": "Whether the caller can manage this broadcast (resend / remind / archive / edit / stats)."
          },
          "can_view_tracking": {
            "type": "boolean",
            "description": "Whether the caller may open the per-recipient tracking details (viewers / acknowledgement response-tracking roster) — the mobile equivalent of the web \"View stats\" page. Matches the gate on GET /broadcasts/{id}/acknowledgements (can-manage, not published-gated), so the client shows the affordance only when that call will succeed. Returned by GET /broadcasts/{id} only.\n"
          },
          "view_count": {
            "type": "integer",
            "description": "Raw view tally (column). Returned by the compose responses\n(create / update / publish) only — NOT by GET /broadcasts/{id},\nwhose detail screen uses `unique_view_count` instead.\n"
          },
          "created_by_id": {
            "type": "integer",
            "description": "Author id. Returned by the compose responses only — NOT by\nGET /broadcasts/{id} (the author id is already in `created_by.id`).\n"
          },
          "attachments": {
            "type": "array",
            "description": "Broadcast file attachments (DriveItem media).",
            "items": {
              "$ref": "#/components/schemas/BroadcastAttachment"
            }
          },
          "reactions": {
            "type": "array",
            "description": "Reaction tallies on this broadcast, one entry per emoji.",
            "items": {
              "type": "object",
              "required": [
                "emoji",
                "count"
              ],
              "properties": {
                "emoji": {
                  "type": "string"
                },
                "count": {
                  "type": "integer"
                }
              }
            }
          },
          "has_reacted": {
            "type": "boolean",
            "description": "Whether the CALLING user has reacted (any emoji) on this broadcast. Returned by GET /broadcasts/{id} only."
          },
          "my_reactions": {
            "type": "array",
            "description": "The emoji(s) the CALLING user has reacted with on this broadcast\n(empty array when none). Same shape as the `my_reactions` field on\nPOST /broadcasts/{id}/reactions/toggle, so the detail screen can\nrestore the user's selection on load. Returned by GET /broadcasts/{id} only.\n",
            "items": {
              "type": "string",
              "example": "👍"
            }
          },
          "comments_count": {
            "type": "integer",
            "description": "Number of (non-deleted) comments on this broadcast."
          },
          "unique_view_count": {
            "type": "integer",
            "description": "Distinct users who have viewed this broadcast."
          },
          "recent_comments": {
            "type": "array",
            "description": "The last 5 top-level comments, newest first.",
            "items": {
              "$ref": "#/components/schemas/BroadcastComment"
            }
          },
          "can_archive": {
            "type": "boolean",
            "description": "Whether the caller can archive this broadcast. Mirrors the web archive\nguard exactly — true only for a PUBLISHED broadcast the caller can\nmanage (admin/manager, or the author when a manager+). Returned by\nGET /broadcasts/{id} only.\n"
          },
          "allow_comments": {
            "type": "boolean",
            "description": "Whether commenting is enabled on this broadcast (drives the comment composer). Returned by GET /broadcasts/{id} only."
          },
          "allow_reactions": {
            "type": "boolean",
            "description": "Whether reacting is enabled on this broadcast (drives the reaction bar). Returned by GET /broadcasts/{id} only."
          },
          "total_recipients": {
            "type": "integer",
            "description": "Recipients captured in the publish-time snapshot. Only when the caller can view stats."
          },
          "acknowledged_count": {
            "type": "integer",
            "description": "How many recipients have acknowledged. Only when the caller can view stats."
          },
          "acknowledged_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the CALLING user acknowledged (null if they haven't; present when require_acknowledgment)."
          }
        }
      },
      "BroadcastAttachment": {
        "type": "object",
        "required": [
          "id",
          "filename"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "filename": {
            "type": "string"
          },
          "content_type": {
            "type": "string",
            "nullable": true
          },
          "byte_size": {
            "type": "integer"
          },
          "url": {
            "type": "string",
            "description": "ActiveStorage blob URL (attachment disposition)."
          }
        }
      },
      "BroadcastComment": {
        "type": "object",
        "required": [
          "id",
          "body",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "body": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "author": {
            "$ref": "#/components/schemas/BroadcastUser"
          },
          "attachments": {
            "type": "array",
            "description": "This comment's file attachments (DriveItem media).",
            "items": {
              "$ref": "#/components/schemas/BroadcastAttachment"
            }
          },
          "can_edit": {
            "type": "boolean",
            "description": "Whether the CALLING user can edit this comment — true only when they authored it."
          },
          "can_delete": {
            "type": "boolean",
            "description": "Whether the CALLING user can delete this comment. Mirrors the web\ncomment \"delete\" visibility — true for the comment's author OR an\nadmin/above member.\n"
          }
        }
      },
      "BroadcastCommentWriteResult": {
        "type": "object",
        "description": "Response for comment create / update.",
        "required": [
          "success",
          "comment"
        ],
        "properties": {
          "success": {
            "type": "boolean"
          },
          "held": {
            "type": "boolean",
            "description": "True when content moderation held the comment for review (hidden until approved)."
          },
          "message": {
            "type": "string"
          },
          "comment": {
            "$ref": "#/components/schemas/BroadcastComment"
          }
        }
      },
      "BroadcastInput": {
        "type": "object",
        "description": "Writable broadcast attributes. On create (Send Now) supply at least one\nrecipient target: `audience_id`, `extra_user_ids`, `audience_criteria`,\nor the top-level `notification_recipient_group_ids`.\n\nReferenced by `PATCH /broadcasts/{id}` only — the POST body inlines its\nown copy (with `description` required). Nothing is required here because\nPATCH is a PARTIAL edit: send just the attributes you are changing.\nMarking `title` and `description` required stopped a generated client\nfrom expressing the edit this endpoint is built for (toggling\n`publish_to_signage`, flipping `allow_comments`) without resending the\nwhole body.\n\n`extra_user_ids` is accepted on CREATE only. Recipients and the\n`channels` mix are fixed at create time; sending either on a PATCH\nchanges nothing and comes back named in `warnings`.\n",
        "properties": {
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "is_critical": {
            "type": "boolean"
          },
          "require_acknowledgment": {
            "type": "boolean"
          },
          "allow_comments": {
            "type": "boolean"
          },
          "allow_reactions": {
            "type": "boolean"
          },
          "audience_id": {
            "type": "integer",
            "nullable": true,
            "description": "A saved CommsHub audience to send to."
          },
          "extra_user_ids": {
            "type": "array",
            "description": "Specific user ids to send to (in addition to any groups/audience).",
            "items": {
              "type": "integer"
            }
          },
          "audience_criteria": {
            "type": "array",
            "description": "Attribute-based audience filters, each a typed hash — e.g.\n`{ \"type\": \"role\", \"roles\": [\"member\"] }`,\n`{ \"type\": \"job_title\", \"titles\": [\"Area Manager\"] }`,\n`{ \"type\": \"department\", \"ids\": [1,2] }`,\n`{ \"type\": \"location\", \"ids\": [3] }`.\n",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "publish_to_signage": {
            "type": "boolean",
            "description": "Mark this broadcast for the break-room screen (Digital Signage)\nrotation — see the POST /broadcasts request body for what the\nmarking does and does not promise.\n\nPATCH semantics: OMIT the key to leave the current selection alone;\nsend `false` to take the broadcast back off the screens. The marker\nis removed rather than stored as `false`.\n"
          },
          "signage_location_ids": {
            "type": "array",
            "description": "Narrow `publish_to_signage` to specific sites. Empty means every\nscreen. Ids outside the caller's business are dropped, and the key\nis ignored unless `publish_to_signage` is on.\n",
            "items": {
              "type": "integer"
            }
          }
        }
      },
      "BroadcastListMeta": {
        "type": "object",
        "description": "Pagination + segment counts for the list endpoint (parity with\nGET /inspections meta). All segment_counts are NOT-READ counts over the\nreceived inbox, independent of the active `filter`:\n  * all         — received & not read by the caller\n  * critical    — received & marked critical & not read\n  * acknowledge — received & acknowledgment-required & not read\n(`all` is itself the unread total, so there is no separate `unread` key.)\n",
        "required": [
          "total_count",
          "total_pages",
          "current_page",
          "per_page",
          "segment_counts"
        ],
        "properties": {
          "total_count": {
            "type": "integer"
          },
          "total_pages": {
            "type": "integer"
          },
          "current_page": {
            "type": "integer"
          },
          "per_page": {
            "type": "integer"
          },
          "segment_counts": {
            "type": "object",
            "required": [
              "all",
              "critical",
              "acknowledge"
            ],
            "properties": {
              "all": {
                "type": "integer"
              },
              "critical": {
                "type": "integer"
              },
              "acknowledge": {
                "type": "integer"
              }
            }
          }
        }
      },
      "ChatAskAiIdentity": {
        "type": "object",
        "description": "The Ask AI assistant's admin-customizable identity. Shared shape for the boot config's ask_ai_account / ask_ai_chat and for GET /chat/ask_ai/identity. Blank description / history_tip mean \"no custom copy\" — the client keeps its own default subtitle and tip.",
        "properties": {
          "id": {
            "type": "integer",
            "nullable": true,
            "description": "Assistant principal's user id; null until provisioned."
          },
          "name": {
            "type": "string"
          },
          "avatar_url": {
            "type": "string",
            "nullable": true,
            "description": "Custom uploaded icon only; null ⇒ use the client's own glyph."
          },
          "avatar_updated_at": {
            "type": "integer",
            "nullable": true,
            "description": "Epoch seconds — cache-busting stamp."
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "history_tip": {
            "type": "string",
            "nullable": true
          },
          "history_search": {
            "type": "boolean",
            "description": "ask_ai_account only — retrieval-grounded answers are live for this tenant."
          }
        }
      },
      "CompanyStoreViewer": {
        "type": "object",
        "description": "Who is asking. THE only place role appears in a Company Store payload.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "image": {
            "type": "string",
            "nullable": true,
            "description": "Absolute avatar URL, or null."
          },
          "is_manager": {
            "type": "boolean",
            "description": "Has a redemption approval queue — a designated approver-group member, or a line manager who has not been superseded by one. This is the ONLY difference between the design's Employee and Manager personas (\"everything above, plus approves orders\"), and it is about the queue, not the job title: a `manager`-role user with no direct reports is `false`. On the dashboard it also decides whether `team_approvals` is present; on the catalog it changes nothing."
          },
          "is_store_admin": {
            "type": "boolean",
            "description": "A business admin-or-above, or a Company Store app-admin. Offer the admin surfaces the web nav offers the same people. No catalog or dashboard field differs for them."
          }
        }
      },
      "CompanyStoreFeatures": {
        "type": "object",
        "description": "The TENANT switches that decide which surfaces exist at all. Hide a section on `features`, disable a control on `viewer`.",
        "properties": {
          "points_redemption_enabled": {
            "type": "boolean",
            "description": "Defaults to ON."
          },
          "cash_purchases_enabled": {
            "type": "boolean",
            "description": "Defaults to OFF — a tenant that never opted in must not be shown card checkout."
          },
          "mixed_payments_enabled": {
            "type": "boolean",
            "description": "Defaults to OFF."
          },
          "recognition_integration_enabled": {
            "type": "boolean"
          },
          "regions_enabled": {
            "type": "boolean",
            "description": "True when the tenant has at least one active region."
          },
          "points_per_dollar": {
            "type": "integer",
            "description": "Conversion rate for a split-payment slider. Coerced to a positive integer server-side."
          },
          "grid_limit": {
            "type": "integer",
            "description": "How many cards each fixed-size grid holds (the tenant's `featured_items_count`, clamped), so a carousel can size itself to what it will receive. Present on the DASHBOARD only — a paginated screen reports `meta.per_page` instead, so promising a grid size there would be a number with nothing to size."
          }
        }
      },
      "CompanyStoreRegion": {
        "type": "object",
        "nullable": true,
        "description": "The region this payload was scoped to. Null when regions are off — and also when they are on but this caller resolves to none, in which case the pool narrowed to global items only. `features.regions_enabled` distinguishes the two.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "country_code": {
            "type": "string"
          }
        }
      },
      "CompanyStoreBalance": {
        "type": "object",
        "description": "The caller's wallet.",
        "properties": {
          "points_balance": {
            "type": "integer"
          },
          "pending_points": {
            "type": "integer"
          },
          "lifetime_points_earned": {
            "type": "integer"
          },
          "lifetime_points_spent": {
            "type": "integer"
          },
          "last_earned_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "last_spent_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "CompanyStoreContext": {
        "type": "object",
        "description": "The blocks every catalog response reports. Identical for every role — read `viewer` for who is asking and `features` for what the tenant switched on.",
        "properties": {
          "viewer": {
            "$ref": "#/components/schemas/CompanyStoreViewer"
          },
          "features": {
            "$ref": "#/components/schemas/CompanyStoreFeatures"
          },
          "region": {
            "$ref": "#/components/schemas/CompanyStoreRegion"
          },
          "available_regions": {
            "type": "array",
            "description": "Every active region, for a client's own picker. Empty when regions are off. The web only renders a switcher when there is more than one.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "name": {
                  "type": "string"
                },
                "country_code": {
                  "type": "string"
                },
                "active": {
                  "type": "boolean",
                  "description": "True for the region this payload was scoped to."
                }
              }
            }
          },
          "balance": {
            "$ref": "#/components/schemas/CompanyStoreBalance"
          },
          "redemption": {
            "type": "object",
            "properties": {
              "approval_threshold_points": {
                "type": "integer",
                "nullable": true,
                "description": "The points figure AT OR ABOVE which a redemption is HELD for approval instead of spending immediately — the lowest enabled tier. Inclusive: the checkout service compares `>=`, so an item priced exactly at this figure IS held, and `requires_approval` on an item detail agrees. Null when the tenant has no approval tier on. Disclose it BEFORE a one-tap redeem: that flow has nowhere else to say so."
              }
            }
          }
        }
      },
      "CompanyStoreFilters": {
        "type": "object",
        "description": "The whole filter sheet — what is applied, and every alternative.",
        "properties": {
          "applied": {
            "type": "object",
            "description": "What the server actually applied. An unrecognised `category` / `sort` / `collection` is dropped, and this block is how a client sees that.",
            "properties": {
              "category": {
                "type": "string",
                "nullable": true
              },
              "collection": {
                "type": "string",
                "nullable": true
              },
              "featured": {
                "type": "boolean"
              },
              "search": {
                "type": "string",
                "nullable": true
              },
              "sort": {
                "type": "string"
              },
              "region_id": {
                "type": "integer",
                "nullable": true
              }
            }
          },
          "sort_options": {
            "type": "array",
            "description": "The same six strategies the desktop sort dropdown offers, in its order.",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string",
                  "enum": [
                    "featured",
                    "popular",
                    "price_low",
                    "price_high",
                    "name",
                    "newest"
                  ]
                },
                "label": {
                  "type": "string"
                },
                "active": {
                  "type": "boolean"
                }
              }
            }
          },
          "categories": {
            "type": "array",
            "description": "\"All Categories\" first with the true total, then one row per ENABLED category — INCLUDING rows at 0, so the sheet does not reshuffle as stock moves. Counts deliberately do NOT apply the active category or collection, because each row is an ALTERNATIVE to the current filter; they DO apply region, audience, category enablement and `search`. Use `has_items` to hide empty rows the way the web dropdown does.",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string",
                  "nullable": true,
                  "description": "Null on the \"All Categories\" row."
                },
                "label": {
                  "type": "string",
                  "description": "The storefront's merchandising label — \"Donate\", not \"Charitable\"; \"Gift Cards\", not \"Gift Card\"."
                },
                "icon": {
                  "type": "string",
                  "description": "Font Awesome name, the same glyph the web chip draws."
                },
                "color": {
                  "type": "string",
                  "description": "Bootstrap colour token, the same one the web chip uses."
                },
                "count": {
                  "type": "integer"
                },
                "has_items": {
                  "type": "boolean"
                },
                "active": {
                  "type": "boolean"
                }
              }
            }
          },
          "collections": {
            "type": "array",
            "description": "Merchandised collection chips, busiest first. Only collections that actually hold an item appear — unlike categories these are free-form per-tenant tags, so there is no fixed vocabulary to render an empty row for.",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string"
                },
                "label": {
                  "type": "string"
                },
                "count": {
                  "type": "integer"
                },
                "active": {
                  "type": "boolean"
                }
              }
            }
          }
        }
      },
      "CompanyStorePageMeta": {
        "type": "object",
        "properties": {
          "current_page": {
            "type": "integer"
          },
          "per_page": {
            "type": "integer"
          },
          "total_count": {
            "type": "integer"
          },
          "total_pages": {
            "type": "integer"
          },
          "has_next_page": {
            "type": "boolean"
          },
          "has_prev_page": {
            "type": "boolean"
          }
        }
      },
      "CompanyStoreItemCard": {
        "type": "object",
        "description": "One item as every Company Store grid renders it. IDENTICAL for every role — an employee, a manager and a store admin get the same fields with the same values for the same request. What varies per caller is the affordability block, `wishlisted`, `can_quick_redeem` and which items appear at all (audience + region).\n**Presentation extras are CATALOG-ONLY.** `category_label`, `category_icon`, `category_color`, `stock_label`, `stock_detail_label`, `has_variants`, `variant_types`, `digital_delivery`, `personalizable`, `wishlisted` and `can_quick_redeem` are added by the catalog endpoints on top of the shared card, because the catalog is the screen that renders chips, stock copy and a one-tap redeem button. The dashboard's `within_reach`, `featured_items` and `saved_items.items` carry every OTHER field here — identity, image, status, availability, region, the payment-gated prices and the affordability block — and omit the eleven above. Treat them as optional rather than assuming a grid card has them.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "category": {
            "type": "string",
            "enum": [
              "swag",
              "gift_card",
              "experience",
              "charitable"
            ]
          },
          "category_label": {
            "type": "string",
            "description": "The storefront's merchandising label (\"Donate\", \"Gift Cards\"). Catalog endpoints only."
          },
          "category_icon": {
            "type": "string",
            "description": "Catalog endpoints only."
          },
          "category_color": {
            "type": "string",
            "description": "Catalog endpoints only."
          },
          "collection": {
            "type": "string",
            "nullable": true
          },
          "image_url": {
            "type": "string",
            "nullable": true,
            "description": "Absolute URL. Prefers an Asset Library image, then an upload, then a provider catalog URL."
          },
          "featured": {
            "type": "boolean"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "out_of_stock",
              "discontinued",
              "draft",
              "coming_soon"
            ]
          },
          "status_label": {
            "type": "string"
          },
          "available": {
            "type": "boolean",
            "description": "Active AND in stock. Everything buyable is gated on this."
          },
          "low_stock": {
            "type": "boolean"
          },
          "inventory_count": {
            "type": "integer",
            "nullable": true,
            "description": "Null means unlimited (print-on-demand, gift cards)."
          },
          "stock_label": {
            "type": "string",
            "description": "\"In stock\" / \"Low stock\" when the item is available. When it is not, the copy names the actual state rather than always saying out of stock: \"Out of stock\" for a stocked-out item, \"Not yet published\" for `draft` / `coming_soon`, \"No longer available\" for `discontinued` — because promising a restock that is never coming is worse than saying nothing. Same three-way split the web detail page renders. Catalog endpoints only."
          },
          "stock_detail_label": {
            "type": "string",
            "description": "The count-carrying form — \"Low stock (4 left)\". Two strings rather than one so a card and a detail screen can differ without either client building copy out of a count. Catalog endpoints only."
          },
          "requires_shipping": {
            "type": "boolean",
            "description": "Admin-editable per item; falls back to the category default (only swag ships)."
          },
          "digital_delivery": {
            "type": "boolean",
            "description": "Catalog endpoints only."
          },
          "personalizable": {
            "type": "boolean",
            "description": "Engraved per person — the engraving details are collected at checkout. Catalog endpoints only."
          },
          "has_variants": {
            "type": "boolean",
            "description": "Catalog endpoints only."
          },
          "variant_types": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The variant types a client must collect before checkout. Empty when there are none. Catalog endpoints only."
          },
          "region": {
            "type": "object",
            "nullable": true,
            "description": "Null for a global item.",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "points_price": {
            "type": "integer",
            "nullable": true,
            "description": "Present only while points redemption is ON and the item carries a points price. Null is a gate, not a missing value."
          },
          "cash_price": {
            "type": "number",
            "format": "float",
            "nullable": true,
            "description": "Present only while card purchases are ON and the item carries a cash price."
          },
          "price_label": {
            "type": "string",
            "description": "The price cell's copy, under the same gates — \"4500 pts\", \"4500 pts or $45.00\", \"$45.00\", or \"Not currently redeemable\" when no payment path is open."
          },
          "redeemable_with_points": {
            "type": "boolean"
          },
          "purchasable_with_cash": {
            "type": "boolean"
          },
          "affordable": {
            "type": "boolean",
            "description": "Against THIS caller's balance."
          },
          "points_to_go": {
            "type": "integer",
            "nullable": true,
            "description": "Shortfall in points. Null when there is no open points path."
          },
          "progress_percent": {
            "type": "integer",
            "description": "0..100 for an affordability bar. 0 when there is no open points path."
          },
          "ready_to_redeem": {
            "type": "boolean",
            "description": "Affordable AND available. Both halves matter — a wishlist keeps out-of-stock items, which would otherwise read \"ready\"."
          },
          "wishlisted": {
            "type": "boolean",
            "description": "Catalog endpoints only."
          },
          "can_quick_redeem": {
            "type": "boolean",
            "description": "The server would accept a one-tap redemption right now: points on, a points price, an enabled category, in stock, no variants, no shipping, no engraving, and affordable. Same guard set the redeem endpoint enforces. Catalog endpoints only."
          },
          "url": {
            "type": "string",
            "description": "Absolute web URL for the item, so a client never hardcodes store paths."
          }
        }
      },
      "CompanyStoreItemDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CompanyStoreItemCard"
          },
          {
            "type": "object",
            "properties": {
              "large_image_url": {
                "type": "string",
                "nullable": true,
                "description": "Higher-resolution hero. Absolute."
              },
              "photos": {
                "type": "array",
                "description": "The primary image first, then the Asset Library gallery rows, in display order. Absolute URLs.",
                "items": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string"
                    },
                    "alt": {
                      "type": "string"
                    }
                  }
                }
              },
              "variants": {
                "type": "array",
                "description": "The variant pickers, in the item's own declared order.",
                "items": {
                  "type": "object",
                  "properties": {
                    "type": {
                      "type": "string"
                    },
                    "label": {
                      "type": "string",
                      "description": "Resolved server-side, so an unlabelled type reads \"Size\", not \"size\"."
                    },
                    "required": {
                      "type": "boolean"
                    },
                    "options": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "item_details": {
                "type": "object",
                "description": "The web detail page's \"Item Details\" list.",
                "properties": {
                  "category": {
                    "type": "string"
                  },
                  "category_label": {
                    "type": "string"
                  },
                  "sku": {
                    "type": "string",
                    "nullable": true
                  },
                  "provider_type": {
                    "type": "string"
                  },
                  "provider_label": {
                    "type": "string"
                  },
                  "requires_shipping": {
                    "type": "boolean"
                  },
                  "estimated_delivery_days": {
                    "type": "integer",
                    "nullable": true
                  },
                  "delivery_label": {
                    "type": "string",
                    "nullable": true,
                    "description": "\"5 business days\" / \"Shipping required\" for a shipped item, \"Instant digital delivery\" for a gift card, null when neither applies."
                  },
                  "fair_market_value": {
                    "type": "number",
                    "format": "float",
                    "nullable": true,
                    "description": "The figure a tax statement reports against a redemption."
                  }
                }
              },
              "payment_options": {
                "type": "array",
                "description": "One entry per path the ITEM has a price for. A method the item cannot be bought with at all is omitted; a method the TENANT switched off is present with a reason.",
                "items": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string",
                      "enum": [
                        "points",
                        "cash",
                        "mixed"
                      ]
                    },
                    "label": {
                      "type": "string"
                    },
                    "points_required": {
                      "type": "integer",
                      "nullable": true
                    },
                    "cash_required": {
                      "type": "number",
                      "format": "float",
                      "nullable": true
                    },
                    "available": {
                      "type": "boolean"
                    },
                    "block_reason": {
                      "type": "string",
                      "nullable": true,
                      "description": "Why not, in the caller's words — \"Insufficient points\", \"Card purchases aren't enabled for this store\", \"Split payments aren't enabled for this store\", the item's own unavailable copy (\"Out of stock\", or \"Not yet published\" / \"No longer available\" for a `draft`/`coming_soon` / `discontinued` item — same wording as `stock_label`), or the neutral \"This item isn't available for redemption right now.\" for a switched-off category (deliberately the same copy the audience refusal uses, so neither discloses the other)."
                    }
                  }
                }
              },
              "requires_approval": {
                "type": "boolean",
                "description": "Would redeeming this item's points price be HELD for approval — `points_price >= redemption.approval_threshold_points`, the same inclusive comparison the checkout service makes. False when no tier is enabled."
              },
              "category_enabled": {
                "type": "boolean",
                "description": "False for a deep link into a category the admin switched off. The item still renders — the web page does too — but nothing about it is buyable."
              },
              "restock_watch": {
                "type": "boolean",
                "description": "Whether the caller has a back-in-stock (\"notify me\") watch on this item."
              },
              "related_items": {
                "type": "array",
                "description": "Up to four more from the same category, under the same region and audience filters as the grid — an item whose own page would refuse must not appear here either.",
                "items": {
                  "$ref": "#/components/schemas/CompanyStoreItemCard"
                }
              }
            }
          }
        ]
      },
      "CompanyStoreOrderRow": {
        "type": "object",
        "description": "One order row. `item` is nil-tolerant: an order whose item row has since been deleted still renders, with `item_name` falling back to \"Item\".",
        "properties": {
          "id": {
            "type": "integer"
          },
          "order_number": {
            "type": "string"
          },
          "item": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "image_url": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "item_name": {
            "type": "string"
          },
          "quantity": {
            "type": "integer"
          },
          "status": {
            "type": "string"
          },
          "status_label": {
            "type": "string",
            "description": "Through the canonical helper, so `pending_approval` reads \"Awaiting Approval\" here exactly as it does on every other store surface."
          },
          "points_spent": {
            "type": "integer"
          },
          "cash_amount": {
            "type": "number",
            "format": "float"
          },
          "total_label": {
            "type": "string"
          },
          "placed_at": {
            "type": "string",
            "format": "date-time"
          },
          "url": {
            "type": "string"
          }
        }
      },
      "CompanyStoreApprovalRequest": {
        "type": "object",
        "description": "One row of the manager Team Approvals widget, oldest-waiting first.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "order_number": {
            "type": "string"
          },
          "requester": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "title": {
                "type": "string",
                "nullable": true
              },
              "image": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "item_name": {
            "type": "string"
          },
          "points": {
            "type": "integer"
          },
          "quantity": {
            "type": "integer"
          },
          "waiting_since": {
            "type": "string",
            "format": "date-time",
            "description": "What the widget sorts by — a manager is holding these up."
          },
          "status": {
            "type": "string"
          },
          "status_label": {
            "type": "string"
          },
          "can_approve": {
            "type": "boolean",
            "description": "Always true for a row in this list: the queue IS the approvable set for this viewer. The decision endpoints still enforce the rule on write, which is where a tier change under a stale list is caught."
          }
        }
      },
      "CompanyStoreError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              },
              "details": {
                "type": "object",
                "nullable": true
              }
            }
          }
        }
      },
      "CompanyStoreApprovalQueue": {
        "type": "object",
        "properties": {
          "viewer": {
            "type": "object",
            "description": "Who is asking. `is_manager` is always `true` in a 200 here (the endpoint 403s otherwise) and is reported so one client model covers every screen in this namespace.",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "image": {
                "type": "string",
                "nullable": true
              },
              "is_manager": {
                "type": "boolean",
                "example": true
              },
              "is_store_admin": {
                "type": "boolean",
                "description": "A business admin-or-above or a Company Store app admin. On THIS endpoint it is load-bearing: it is what widens the pool to the whole tenant at every approval tier (bounded by the token — a narrowed `own_company_store` token reads the manager queue even for an admin, so a `true` here does not by itself prove the wide pool was served; `scope` does)."
              }
            }
          },
          "features": {
            "type": "object",
            "description": "The TENANT switches, the same block the dashboard / catalog / orders endpoints report. Present so a client renders one store shell from any call.",
            "properties": {
              "points_redemption_enabled": {
                "type": "boolean"
              },
              "cash_purchases_enabled": {
                "type": "boolean"
              },
              "mixed_payments_enabled": {
                "type": "boolean"
              },
              "recognition_integration_enabled": {
                "type": "boolean"
              },
              "regions_enabled": {
                "type": "boolean"
              },
              "points_per_dollar": {
                "type": "integer",
                "example": 100
              }
            }
          },
          "scope": {
            "type": "string",
            "enum": [
              "company",
              "team"
            ],
            "description": "`company` — a store admin (every tier) or a designated approver-group member (manager tier); the queue spans the whole tenant. `team` — a line manager; the queue is their own direct reports'. Drives the screen's subtitle: calling a committee's rows \"your team\" mislabels every one of them."
          },
          "filters": {
            "type": "object",
            "description": "What was actually APPLIED, after clamping and fallbacks.",
            "properties": {
              "sort": {
                "type": "string",
                "enum": [
                  "oldest",
                  "newest"
                ]
              },
              "per_page": {
                "type": "integer",
                "example": 20
              }
            }
          },
          "total_pending": {
            "type": "integer",
            "description": "The FULL queue depth — how many redemptions are waiting on this approver in total, independent of `page` / `per_page`. This is the tab badge figure, and it equals the dashboard widget's `team_approvals.pending_count`.",
            "example": 7
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CompanyStoreApprovalQueueRow"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/CompanyStoreApprovalPageMeta"
          }
        }
      },
      "CompanyStoreApprovalQueueRow": {
        "type": "object",
        "description": "One held redemption as the Approvals SCREEN renders it — the shared `CompanyStoreApprovalRequest` card plus the six fields a full-width queue row shows and a four-row dashboard widget doesn't.",
        "properties": {
          "id": {
            "type": "integer",
            "description": "The StoreOrder id — what the decision endpoints take."
          },
          "order_number": {
            "type": "string",
            "example": "ORD-20260731-1AOOOK"
          },
          "requester": {
            "type": "object",
            "description": "The employee whose redemption is held.",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string",
                "example": "Maya Chen"
              },
              "title": {
                "type": "string",
                "nullable": true,
                "example": "Support Specialist"
              },
              "image": {
                "type": "string",
                "nullable": true,
                "description": "Absolute avatar URL."
              }
            }
          },
          "requester_department": {
            "type": "string",
            "nullable": true,
            "description": "The requester's organizational department — the design's \"Support · Jul 18\" sub-line. `null` for a user with no department; the client then prints the date alone.",
            "example": "Support"
          },
          "item_name": {
            "type": "string",
            "description": "Falls back to `\"Item\"` when the item row has since been deleted, so a row always renders.",
            "example": "Company Logo Hoodie"
          },
          "item": {
            "type": "object",
            "nullable": true,
            "description": "`null` when the item row has been deleted.",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "image_url": {
                "type": "string",
                "nullable": true,
                "description": "Absolute URL."
              }
            }
          },
          "points": {
            "type": "integer",
            "description": "The points held on this redemption — what a decline returns.",
            "example": 4500
          },
          "points_label": {
            "type": "string",
            "description": "Pre-formatted with the same delimiter the web table uses, so the surfaces never disagree.",
            "example": "4,500 pts"
          },
          "quantity": {
            "type": "integer",
            "example": 1
          },
          "variant_display": {
            "type": "string",
            "nullable": true,
            "description": "`\"Size: L, Color: Black\"`. `null` when the item has no variants."
          },
          "waiting_since": {
            "type": "string",
            "format": "date-time",
            "description": "When the redemption was placed — what `sort` orders by."
          },
          "waiting_days": {
            "type": "integer",
            "description": "Whole days waited, computed server-side so every client's \"3 days\" agrees with every other's.",
            "example": 3
          },
          "status": {
            "type": "string",
            "example": "pending_approval"
          },
          "status_label": {
            "type": "string",
            "description": "The canonical store copy, via the same helper every other store surface uses — `\"Awaiting Approval\"`, never `\"Pending Approval\"`.",
            "example": "Awaiting Approval"
          },
          "can_approve": {
            "type": "boolean",
            "description": "May THIS caller decide THIS row. `true` for every row on the manager branch, where the queue is the approvable set. On the admin branch the pool is wider than the approvable set, so it can be `false`: a Company Store app admin who is not a business admin sees an admin-tier hold they may not release — the same row the web admin page draws with its buttons disabled. Render the controls off this flag; a decision POST on a `false` row answers `403 not_an_approver`. The decision endpoints enforce the rule on write regardless, which is where a tier change under a stale list is caught."
          },
          "order_url": {
            "type": "string",
            "description": "Absolute deep link to the order's own page."
          }
        }
      },
      "CompanyStoreApprovalDecisionResponse": {
        "type": "object",
        "properties": {
          "approval": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CompanyStoreApprovalRequest"
              },
              {
                "type": "object",
                "properties": {
                  "decision": {
                    "type": "string",
                    "enum": [
                      "approved",
                      "declined"
                    ]
                  },
                  "decided_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "decided_by": {
                    "type": "object",
                    "description": "The approver who took the decision.",
                    "properties": {
                      "id": {
                        "type": "integer"
                      },
                      "name": {
                        "type": "string"
                      },
                      "title": {
                        "type": "string",
                        "nullable": true
                      },
                      "image": {
                        "type": "string",
                        "nullable": true
                      }
                    }
                  },
                  "reason": {
                    "type": "string",
                    "nullable": true,
                    "description": "The decline reason as recorded. `null` on an approve, and on a decline sent without one."
                  },
                  "points_refunded": {
                    "type": "integer",
                    "description": "What went back to the employee's wallet. The held points on a decline; `0` on an approve, where the points stay spent.",
                    "example": 4500
                  },
                  "status": {
                    "type": "string",
                    "description": "The RESULTING order status — `pending` after an approve (now queued for fulfillment), `cancelled` after a decline.",
                    "example": "pending"
                  },
                  "can_approve": {
                    "type": "boolean",
                    "description": "`false` — the row has left the queue."
                  }
                }
              }
            ]
          },
          "total_pending": {
            "type": "integer",
            "description": "The queue depth AFTER this decision, so a client's tab badge updates from this response."
          },
          "message": {
            "type": "string",
            "description": "The same copy the web flash shows, so both surfaces tell an approver the same thing.",
            "example": "Approved"
          },
          "unread_notification_count": {
            "type": "integer"
          }
        }
      },
      "CompanyStoreApprovalPageMeta": {
        "type": "object",
        "description": "The page envelope every paginated endpoint in this namespace answers with. `total_count` equals `total_pending` here, because nothing narrows the queue.",
        "properties": {
          "current_page": {
            "type": "integer"
          },
          "per_page": {
            "type": "integer"
          },
          "total_count": {
            "type": "integer"
          },
          "total_pages": {
            "type": "integer"
          },
          "has_next_page": {
            "type": "boolean"
          },
          "has_prev_page": {
            "type": "boolean"
          }
        }
      },
      "CompanyStoreApprovalError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "access_denied",
                  "store_disabled",
                  "approvals_delegated",
                  "not_an_approver",
                  "not_found",
                  "decision_failed"
                ]
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "CompanyStoreCartLine": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "item": {
            "type": "object",
            "description": "The item card",
            "as the catalog renders it.": null
          },
          "quantity": {
            "type": "integer"
          },
          "max_quantity": {
            "type": "integer",
            "description": "min(5, stock) — the stepper's ceiling."
          },
          "selected_variants": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "unit_points": {
            "type": "integer"
          },
          "unit_cash": {
            "type": "number"
          },
          "line_points": {
            "type": "integer"
          },
          "line_cash": {
            "type": "number"
          },
          "issue": {
            "type": "string",
            "nullable": true,
            "description": "Why this line can't check out as it stands; null when it can."
          }
        }
      },
      "CompanyStoreCartResponse": {
        "type": "object",
        "properties": {
          "cart": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "nullable": true
              },
              "line_count": {
                "type": "integer"
              },
              "units": {
                "type": "integer"
              },
              "fulfillment_route": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "printful",
                  "managed",
                  "internal"
                ]
              },
              "requires_shipping": {
                "type": "boolean"
              },
              "currency_code": {
                "type": "string",
                "nullable": true
              },
              "limits": {
                "type": "object",
                "properties": {
                  "max_lines": {
                    "type": "integer",
                    "example": 5
                  },
                  "max_quantity_per_line": {
                    "type": "integer",
                    "example": 5
                  }
                }
              },
              "lines": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/CompanyStoreCartLine"
                }
              },
              "totals": {
                "type": "object",
                "properties": {
                  "points": {
                    "type": "integer"
                  },
                  "cash": {
                    "type": "number"
                  },
                  "cash_cents": {
                    "type": "integer"
                  }
                }
              },
              "blockers": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Why a points checkout would be refused, in the buyer's words. Empty when it wouldn't."
              }
            }
          },
          "wallet": {
            "type": "object",
            "description": "The caller's balance card."
          },
          "added_line_id": {
            "type": "integer",
            "description": "POST /cart/lines only."
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "CompanyStoreCartError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "unavailable",
                  "ineligible",
                  "route_mismatch",
                  "currency_mismatch",
                  "cart_full",
                  "out_of_stock",
                  "variant_invalid",
                  "region",
                  "line_not_found"
                ]
              },
              "message": {
                "type": "string",
                "description": "The web's own sentence."
              }
            }
          }
        }
      },
      "CompanyStoreCartCheckoutPreview": {
        "type": "object",
        "properties": {
          "checkout": {
            "type": "object",
            "properties": {
              "lines": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/CompanyStoreCartLine"
                }
              },
              "totals": {
                "type": "object",
                "properties": {
                  "points": {
                    "type": "integer"
                  },
                  "cash": {
                    "type": "number"
                  },
                  "cash_cents": {
                    "type": "integer"
                  },
                  "units": {
                    "type": "integer"
                  }
                }
              },
              "currency": {
                "type": "string",
                "nullable": true
              },
              "payment_options": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string",
                      "enum": [
                        "points",
                        "cash",
                        "mixed"
                      ]
                    },
                    "label": {
                      "type": "string"
                    },
                    "available": {
                      "type": "boolean"
                    },
                    "block_reason": {
                      "type": "string"
                    },
                    "amount": {
                      "type": "object",
                      "description": "`{ points }`, `{ cash }` or `{ points_max }` (the split slider's ceiling)."
                    }
                  }
                }
              },
              "selected_payment": {
                "type": "string",
                "nullable": true
              },
              "can_checkout": {
                "type": "boolean"
              },
              "shortfall": {
                "type": "integer",
                "description": "Points short of the total; 0 when covered."
              },
              "wallet": {
                "type": "object"
              },
              "shipping": {
                "type": "object",
                "properties": {
                  "required": {
                    "type": "boolean"
                  },
                  "collected_on_stripe_for": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Payment types for which Stripe collects the address (skip your form for these)."
                  },
                  "prefill": {
                    "type": "object"
                  },
                  "countries_url": {
                    "type": "string"
                  }
                }
              },
              "disclosures": {
                "type": "object",
                "properties": {
                  "required_approver": {
                    "type": "string",
                    "nullable": true
                  },
                  "velocity_hold_reason": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        }
      },
      "CompanyStoreCartCheckoutResponse": {
        "type": "object",
        "properties": {
          "placed": {
            "type": "boolean"
          },
          "order": {
            "type": "object",
            "description": "The order card."
          },
          "held_for_approval": {
            "type": "boolean"
          },
          "payment": {
            "type": "object",
            "description": "Present only when `placed` is false.",
            "properties": {
              "checkout_url": {
                "type": "string"
              },
              "session_id": {
                "type": "string"
              },
              "return_url_prefix": {
                "type": "string"
              }
            }
          },
          "wallet": {
            "type": "object"
          },
          "message": {
            "type": "string"
          }
        }
      },
      "CompanyStoreOrderRequest": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "kind": {
            "type": "string",
            "enum": [
              "question",
              "cancellation",
              "delivery_status",
              "address_change",
              "change_selection",
              "damaged",
              "code_issue",
              "payment",
              "other"
            ]
          },
          "kind_label": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "approved",
              "declined",
              "answered"
            ]
          },
          "message": {
            "type": "string"
          },
          "resolution_notes": {
            "type": "string",
            "nullable": true,
            "description": "The admin's reply or reason, once decided."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "processed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "CompanyStoreCheckoutRequest": {
        "type": "object",
        "required": [
          "payment_type"
        ],
        "properties": {
          "payment_type": {
            "type": "string",
            "enum": [
              "points",
              "cash",
              "mixed"
            ],
            "description": "How to pay. `points` places the order in this request; `cash` and `mixed` return a Stripe URL. Re-checked against the tenant's toggles server-side."
          },
          "quantity": {
            "type": "integer",
            "default": 1,
            "description": "Clamped to 1..99, and bounded again by the item's inventory."
          },
          "points_to_use": {
            "type": "integer",
            "description": "`mixed` only — how many points to put toward the price. **Forced to 0 for `cash`**, so a hidden slider cannot spend points on a card-only submit. If it covers the whole price the service routes the order to the points path and the response comes back `placed: true`."
          },
          "variants": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Required when the item declares options — `{\"size\": \"L\", \"color\": \"Black\"}`. Keys the item never declared are dropped. An incomplete selection is refused with `variants_required`.",
            "example": {
              "size": "L",
              "color": "Black"
            }
          },
          "shipping_address": {
            "$ref": "#/components/schemas/CompanyStoreCheckoutShippingAddress"
          },
          "personalization": {
            "type": "object",
            "description": "Engraving details, for a personalizable item.",
            "properties": {
              "recipient_name": {
                "type": "string"
              },
              "years_of_service": {
                "type": "string"
              },
              "engraving_line": {
                "type": "string"
              }
            }
          },
          "idempotency_key": {
            "type": "string",
            "description": "**Send one.** A client-generated key (a UUID per checkout attempt). A repeat returns the same order rather than placing a second one and debiting the points twice. Scoped to the caller, so another user's key can never resolve to their order.",
            "example": "8f14e45f-ea0b-4c1e-9d8a-2b3c4d5e6f70"
          }
        }
      },
      "CompanyStoreCheckoutShippingAddress": {
        "type": "object",
        "description": "Where it ships. Send when `shipping.required` is true AND `shipping.collected_by` is `app`; when it is `stripe`, Stripe collects the address on its own page and copies it back onto the order.",
        "properties": {
          "name": {
            "type": "string"
          },
          "street1": {
            "type": "string"
          },
          "street2": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "state": {
            "type": "string"
          },
          "zip": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "phone": {
            "type": "string"
          }
        }
      },
      "CompanyStoreCheckoutPaymentOption": {
        "type": "object",
        "description": "One payment path, and the reason it can't be taken right now. A path the item has no price for is omitted entirely; a path the TENANT switched off is present with a reason, because that is a state an admin can change.",
        "properties": {
          "key": {
            "type": "string",
            "enum": [
              "points",
              "cash",
              "mixed"
            ]
          },
          "label": {
            "type": "string",
            "example": "Redeem with Points"
          },
          "points_required": {
            "type": "integer",
            "nullable": true
          },
          "cash_required": {
            "type": "number",
            "nullable": true
          },
          "available": {
            "type": "boolean"
          },
          "block_reason": {
            "type": "string",
            "nullable": true,
            "description": "Buyer-facing reason this path is unavailable — show it instead of a silently greyed-out button. Null when `available`."
          }
        }
      },
      "CompanyStoreCheckoutTotals": {
        "type": "object",
        "description": "What each path costs at the requested quantity.",
        "properties": {
          "points": {
            "type": "object",
            "properties": {
              "points": {
                "type": "integer"
              },
              "cash": {
                "type": "number",
                "example": 0.0
              }
            }
          },
          "cash": {
            "type": "object",
            "properties": {
              "points": {
                "type": "integer",
                "example": 0
              },
              "cash": {
                "type": "number"
              }
            }
          },
          "mixed": {
            "type": "object",
            "properties": {
              "max_points": {
                "type": "integer",
                "description": "The slider ceiling — `min(points needed to cover the whole price, the caller's balance)`. Do not allow past it: there would be no card portion left, and the write would silently become a points redemption."
              },
              "points_per_dollar": {
                "type": "integer",
                "description": "The tenant's conversion rate."
              },
              "requested_points": {
                "type": "integer",
                "description": "Echo of the `points_to_use` this preview was asked about."
              },
              "cash_after_points": {
                "type": "number",
                "description": "What the card is charged for `requested_points`."
              }
            }
          }
        }
      },
      "CompanyStoreCheckoutPreviewResponse": {
        "type": "object",
        "properties": {
          "checkout": {
            "type": "object",
            "properties": {
              "item": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  },
                  "image_url": {
                    "type": "string",
                    "nullable": true
                  },
                  "points_price": {
                    "type": "integer"
                  },
                  "cash_price": {
                    "type": "number"
                  },
                  "currency": {
                    "type": "string",
                    "example": "USD"
                  }
                }
              },
              "quantity": {
                "type": "object",
                "properties": {
                  "selected": {
                    "type": "integer"
                  },
                  "min": {
                    "type": "integer",
                    "example": 1
                  },
                  "max": {
                    "type": "integer",
                    "description": "Bounded by the item's tracked inventory when it tracks any."
                  }
                }
              },
              "payment_options": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/CompanyStoreCheckoutPaymentOption"
                }
              },
              "selected_payment": {
                "type": "string",
                "nullable": true,
                "description": "The path to open on — the caller's requested `payment_type` when this item can take it, otherwise the first available one."
              },
              "can_checkout": {
                "type": "boolean",
                "description": "Is ANY path available. False for an unbuyable item (still a 200)."
              },
              "totals": {
                "$ref": "#/components/schemas/CompanyStoreCheckoutTotals"
              },
              "wallet": {
                "$ref": "#/components/schemas/CompanyStoreCheckoutWallet"
              },
              "variants": {
                "type": "array",
                "description": "The option pickers to render, in the item's declared order.",
                "items": {
                  "type": "object",
                  "properties": {
                    "type": {
                      "type": "string",
                      "example": "size"
                    },
                    "label": {
                      "type": "string",
                      "example": "Size"
                    },
                    "required": {
                      "type": "boolean"
                    },
                    "options": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "shipping": {
                "type": "object",
                "properties": {
                  "required": {
                    "type": "boolean"
                  },
                  "collected_by": {
                    "type": "string",
                    "enum": [
                      "app",
                      "stripe"
                    ],
                    "description": "`stripe` when Stripe Tax is on for a shippable item — Stripe collects the address on its own page so `automatic_tax` can price destination tax, and your form must skip its address step. Points-only orders never reach Stripe, so they always collect in-app."
                  },
                  "collected_by_stripe_for": {
                    "type": "array",
                    "description": "Which payment paths Stripe collects for (`[]`, or cash+mixed).",
                    "items": {
                      "type": "string"
                    }
                  },
                  "fields": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "regions": {
                    "type": "object",
                    "description": "The two address dropdowns' CONTENTS — the shippable countries and, nested under each, its states/provinces. `fields` above names the boxes; this says what each one accepts, which is what stops a native form from free-texting a state the fulfillment path reads as a 2-letter `state_code`.\n\nPresent whenever `required` is true — including when `collected_by` is `stripe`, because that collector is per PAYMENT PATH (`collected_by_stripe_for` is only cash/mixed) and the points-only path on the same item always collects in-app. Absent on a digital item, which has no shipping section.\n\nByte-identical to the `shipping_countries` payload of `GET /company-store/shipping-countries` (one server-side builder feeds both), so the normal purchase flow needs no extra round trip — call that endpoint only to cache the list at launch or to render an address form outside a checkout. Read its documentation for the rule that matters: country submits the display NAME, state submits the 2-letter CODE.",
                    "properties": {
                      "countries": {
                        "type": "array",
                        "description": "Never empty. Render whatever length this holds rather than assuming one entry.",
                        "items": {
                          "$ref": "#/components/schemas/CompanyStoreShippingCountry"
                        }
                      },
                      "default_country": {
                        "type": "string",
                        "description": "The country to select when the caller has expressed no preference — always one of the rows' `value`s.",
                        "example": "United States"
                      },
                      "prefill": {
                        "$ref": "#/components/schemas/CompanyStoreShippingPrefill"
                      }
                    }
                  }
                }
              },
              "personalization": {
                "type": "object",
                "description": "The engraving block. `required` is whether this item is engraved at all; the per-field flags below say which boxes the server actually insists on, so a form need not mark all three required.",
                "properties": {
                  "required": {
                    "type": "boolean"
                  },
                  "fields": {
                    "type": "array",
                    "description": "One entry per engraving field, with the server's own rules. Enforce `max_length` in the form: the service refuses past it (`checkout_failed`), and discovering that after the buyer has committed is the whole reason it is disclosed here.",
                    "items": {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string",
                          "example": "recipient_name"
                        },
                        "label": {
                          "type": "string",
                          "example": "Recipient name"
                        },
                        "required": {
                          "type": "boolean",
                          "description": "Today only `recipient_name` is required."
                        },
                        "max_length": {
                          "type": "integer",
                          "example": 80
                        }
                      }
                    }
                  }
                }
              },
              "category_enabled": {
                "type": "boolean",
                "description": "False when the admin has switched this item's category off. The item still renders here (with every `payment_options` entry blocked) so a deep link is not a dead end, but the write refuses with `item_unavailable`. Same field GET /company-store/catalog/{id} reports."
              },
              "disclosures": {
                "$ref": "#/components/schemas/CompanyStoreCheckoutDisclosures"
              }
            }
          }
        }
      },
      "CompanyStoreCheckoutDisclosures": {
        "type": "object",
        "description": "What will happen when the buyer commits — said before they commit, which for a one-tap redeem is the only place it can be said.",
        "properties": {
          "requires_approval": {
            "type": "boolean",
            "description": "Will this redemption be HELD for approval instead of spending immediately."
          },
          "approver_tier": {
            "type": "string",
            "nullable": true,
            "enum": [
              "admin",
              "manager",
              null
            ],
            "description": "Which tier the hold routes to. Null when nothing holds it."
          },
          "velocity_hold_reason": {
            "type": "string",
            "nullable": true,
            "description": "The short-window fraud brake tripped. It does not block the order, it forces an admin hold — disclose it as \"this may need approval\", not as a refusal."
          },
          "applies_to": {
            "type": "array",
            "description": "Which payment paths the hold applies to. A hold is on the POINTS portion, so a pure-card checkout is never held.",
            "items": {
              "type": "string"
            }
          },
          "monthly_points_cap": {
            "type": "integer",
            "nullable": true
          },
          "monthly_points_remaining": {
            "type": "integer",
            "nullable": true
          }
        }
      },
      "CompanyStoreCheckoutWallet": {
        "type": "object",
        "description": "The caller's points wallet. On a write response this is the balance AFTER the debit (or, for an abandon, after the refund) — render from it rather than re-asking /config, which would show a stale number. Same six fields as `CompanyStoreBalance` in company_store.yaml — both are the namespace's one `balance_card`, so a client parses one shape.",
        "properties": {
          "points_balance": {
            "type": "integer"
          },
          "pending_points": {
            "type": "integer"
          },
          "lifetime_points_earned": {
            "type": "integer"
          },
          "lifetime_points_spent": {
            "type": "integer"
          },
          "last_earned_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "last_spent_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "CompanyStoreCheckoutPayment": {
        "type": "object",
        "description": "Present only when `placed` is false. What to open, and where to confirm.",
        "properties": {
          "provider": {
            "type": "string",
            "enum": [
              "stripe"
            ]
          },
          "checkout_url": {
            "type": "string",
            "description": "Open this in the system browser (ASWebAuthenticationSession / Chrome Custom Tabs). Not a WebView you built — the buyer needs to see the real stripe.com address.",
            "example": "https://checkout.stripe.com/c/pay/cs_test_a1B2c3"
          },
          "session_id": {
            "type": "string",
            "example": "cs_test_a1B2c3"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Stripe's own session expiry (24h). After it, confirm reports the order's real state."
          },
          "currency": {
            "type": "string",
            "example": "USD"
          },
          "cash_amount": {
            "type": "number",
            "description": "What the card will be charged."
          },
          "points_held": {
            "type": "integer",
            "description": "The points portion",
            "already debited.": null
          },
          "return_url_prefix": {
            "type": "string",
            "description": "Watch the in-browser navigation for this PREFIX (Stripe appends its own `?session_id=`), then close the browser and call `complete_url`. Intercepting is an optimisation — polling `complete_url` works without it."
          },
          "cancel_url_prefix": {
            "type": "string",
            "description": "Same, for the buyer pressing Stripe's back link — then call `abandon_url`."
          },
          "complete_url": {
            "type": "string",
            "description": "Idempotent. Poll it."
          },
          "abandon_url": {
            "type": "string"
          }
        }
      },
      "CompanyStoreCheckoutWriteResponse": {
        "type": "object",
        "description": "The response to the write. **Branch on `placed`** — a `mixed` checkout the points fully covered comes back placed with no `payment` block.",
        "properties": {
          "placed": {
            "type": "boolean",
            "description": "True — the order is placed, nothing further is owed. False — open `payment.checkout_url`.\n`placed` is derived from the ORDER's own state, never from whether a `payment` block happens to be present, so `true` can always be trusted as \"nothing is owed\". A 200 therefore never carries `false` without a `payment` block: the one case where an order exists, money is owed and no session can be returned (an idempotent retry whose Stripe session expired or closed) is answered `409 checkout_session_unavailable` instead."
          },
          "order": {
            "$ref": "#/components/schemas/CompanyStoreCheckoutOrderCard"
          },
          "requires_approval": {
            "type": "boolean",
            "description": "Points paths only. True means the order is `pending_approval` with the points HELD — report the hold, do not celebrate."
          },
          "payment": {
            "$ref": "#/components/schemas/CompanyStoreCheckoutPayment"
          },
          "wallet": {
            "$ref": "#/components/schemas/CompanyStoreCheckoutWallet"
          },
          "message": {
            "type": "string",
            "description": "Buyer-facing confirmation. Safe to show verbatim.",
            "example": "Order"
          }
        }
      },
      "CompanyStoreCheckoutCompleteResponse": {
        "type": "object",
        "properties": {
          "placed": {
            "type": "boolean",
            "example": true
          },
          "order": {
            "$ref": "#/components/schemas/CompanyStoreCheckoutOrderCard"
          },
          "wallet": {
            "$ref": "#/components/schemas/CompanyStoreCheckoutWallet"
          },
          "message": {
            "type": "string",
            "example": "Payment received. Order #ORD-20260819-ABC123 is confirmed."
          },
          "unread_notification_count": {
            "type": "integer",
            "description": "Native app badge count (the shared api/v1 envelope)."
          }
        }
      },
      "CompanyStoreCheckoutAbandonResponse": {
        "type": "object",
        "properties": {
          "cancelled": {
            "type": "boolean",
            "example": true
          },
          "order": {
            "$ref": "#/components/schemas/CompanyStoreCheckoutOrderCard"
          },
          "points_restored": {
            "type": "boolean"
          },
          "points_restored_amount": {
            "type": "integer"
          },
          "wallet": {
            "$ref": "#/components/schemas/CompanyStoreCheckoutWallet"
          },
          "message": {
            "type": "string",
            "example": "Checkout cancelled. 700 points have been returned to your balance."
          },
          "unread_notification_count": {
            "type": "integer",
            "description": "Native app badge count (the shared api/v1 envelope)."
          }
        }
      },
      "CompanyStoreCheckoutOrderCard": {
        "type": "object",
        "description": "The shared Company Store order row — the same shape the dashboard's recent orders and the orders list render, so one order never looks like two different things. Full detail is `GET /company-store/orders/{order_number}`.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "order_number": {
            "type": "string",
            "example": "ORD-20260819-ABC123"
          },
          "item": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "image_url": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "item_name": {
            "type": "string"
          },
          "quantity": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending_approval",
              "pending",
              "processing",
              "fulfilled",
              "cancelled",
              "refunded"
            ]
          },
          "status_label": {
            "type": "string",
            "description": "Canonical label — `pending_approval` reads \"Awaiting Approval\" on every surface."
          },
          "points_spent": {
            "type": "integer"
          },
          "cash_amount": {
            "type": "number"
          },
          "total_label": {
            "type": "string"
          },
          "placed_at": {
            "type": "string",
            "format": "date-time"
          },
          "url": {
            "type": "string"
          }
        }
      },
      "CompanyStoreCheckoutError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "Machine-readable. See each response's description for the codes it can return."
              },
              "message": {
                "type": "string",
                "description": "Buyer-facing. For `checkout_failed` this is the service's own prose (with the real numbers in it) — show it."
              },
              "details": {
                "type": "object",
                "nullable": true,
                "description": "Present on the 409s: `order` carries the order's current state for a polling client, and `cancel_url` on `not_abandonable` points at the endpoint to use instead."
              }
            }
          }
        }
      },
      "CompanyStoreConfigViewer": {
        "type": "object",
        "description": "Who is asking. The namespace's shared viewer card, identical on every Company Store endpoint. This is the ONLY place role appears in this payload.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "image": {
            "type": "string",
            "nullable": true
          },
          "is_manager": {
            "type": "boolean",
            "description": "Does this viewer have a redemption approval queue — Store::RedemptionApprovalService.can_approve_redemptions?, the same predicate the approvals queue's own door gates on. THE flag that gates the manager UI. True for a designated approver-group member, and for a line manager with direct reports when no group is configured; NOT a `role: manager` membership check."
          },
          "is_store_admin": {
            "type": "boolean",
            "description": "A business admin-or-above or a Company Store app-admin. Offers the item / order management surfaces."
          }
        }
      },
      "CompanyStoreConfigFeatures": {
        "type": "object",
        "description": "The TENANT switches that decide which surfaces exist at all. The first six are shared verbatim with the other Company Store endpoints; `points_expiry_enabled` is merged on here for the same reason the Points tab merges it.",
        "properties": {
          "points_redemption_enabled": {
            "type": "boolean",
            "description": "Default true. Whether the store takes points at all."
          },
          "cash_purchases_enabled": {
            "type": "boolean",
            "description": "Default FALSE. A tenant that never opted in must not be told cash checkout exists."
          },
          "mixed_payments_enabled": {
            "type": "boolean",
            "description": "Default false. The RAW admin toggle — NOT whether a split payment can be taken, which additionally needs both of the two flags above. Use `payment_methods` for what is actually offerable."
          },
          "recognition_integration_enabled": {
            "type": "boolean"
          },
          "regions_enabled": {
            "type": "boolean",
            "description": "Whether the tenant has any active region, i.e. whether region scoping is in force."
          },
          "points_per_dollar": {
            "type": "integer",
            "description": "The conversion rate a mixed-payment slider needs. Coerced server-side to a positive Integer (admins have saved it as a string).",
            "example": 100
          },
          "points_expiry_enabled": {
            "type": "boolean",
            "description": "Whether the tenant expires points at all. Distinguishes `points.expiring_points: 0` meaning \"nothing is close\" from \"this tenant does not expire points\"."
          }
        }
      },
      "CompanyStoreConfigRegion": {
        "type": "object",
        "nullable": true,
        "description": "The region this payload was scoped to. null when regions are off, or when they are on and this user resolves to none (the pool then narrowed to global items) — `features.regions_enabled` distinguishes the two.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "country_code": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "CompanyStoreConfigAvailableRegion": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "country_code": {
            "type": "string",
            "nullable": true
          },
          "active": {
            "type": "boolean",
            "description": "True for the region this payload was scoped to."
          }
        }
      },
      "CompanyStoreConfigCategory": {
        "type": "object",
        "description": "One row of the category-filter bottom sheet. The first row of the array is always the \"All Categories\" row, which carries `key: null` and the true total.",
        "properties": {
          "key": {
            "type": "string",
            "nullable": true,
            "description": "The value `?category=` takes VERBATIM — the StoreItem column vocabulary, NOT the merchandising label. null on the \"All Categories\" row.",
            "enum": [
              "swag",
              "gift_card",
              "experience",
              "charitable",
              null
            ]
          },
          "label": {
            "type": "string",
            "description": "The storefront's merchandising label, which deliberately differs from `key` — `charitable` is sold as \"Donate\", and the other three are pluralised.",
            "example": "Gift Cards"
          },
          "icon": {
            "type": "string",
            "description": "A stable FontAwesome-style icon key, never an image URL. `tag` on the All row; `tshirt` / `gift` / `star` / `hand-holding-heart` for the four categories, with `box` as the fallback. The same keys the web storefront renders.",
            "example": "gift"
          },
          "color": {
            "type": "string",
            "description": "Bootstrap contextual colour for the chip, shared with the web.",
            "example": "success"
          },
          "count": {
            "type": "integer",
            "description": "How many available items this row would land on, under the same region / audience / category-enablement filters the catalog listing applies. Computed in ONE grouped query for the whole sheet. A count of 0 means the category is ON and empty — a category the admin switched OFF is absent from the array entirely."
          },
          "has_items": {
            "type": "boolean",
            "description": "`count > 0`. For a client that wants the web dropdown's behaviour of hiding empty categories."
          },
          "active": {
            "type": "boolean",
            "description": "Which row is selected. Always the \"All Categories\" row on this endpoint, since a bootstrap has no applied filter."
          }
        }
      },
      "CompanyStoreConfigPaymentMethod": {
        "type": "object",
        "description": "One payment method the TENANT accepts. Only enabled methods appear. This is the ceiling — an individual item may offer fewer (see the item detail's `payment_options`) but never one outside this array.",
        "properties": {
          "key": {
            "type": "string",
            "enum": [
              "points",
              "cash",
              "mixed"
            ]
          },
          "label": {
            "type": "string",
            "example": "Points"
          },
          "description": {
            "type": "string",
            "example": "Redeem with your points balance"
          }
        }
      },
      "CompanyStoreConfigPoints": {
        "type": "object",
        "description": "The caller's OWN wallet, for every role. Plain-noun keys; the screen endpoints serve the same figures under the namespace's `balance_card` spelling (`points_balance`, `pending_points`, …).",
        "properties": {
          "balance": {
            "type": "integer",
            "description": "Spendable points."
          },
          "pending": {
            "type": "integer",
            "description": "Earned but not yet released."
          },
          "lifetime_earned": {
            "type": "integer"
          },
          "lifetime_spent": {
            "type": "integer"
          },
          "last_earned_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "last_spent_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "points_per_dollar": {
            "type": "integer",
            "description": "Mirrors `features.points_per_dollar`, carried here so the wallet block is self-contained."
          },
          "expiring_points": {
            "type": "integer",
            "description": "What NEWLY expires within `expiring_within_days` — never the whole expirable pool. Always 0 when `features.points_expiry_enabled` is false."
          },
          "expiring_within_days": {
            "type": "integer",
            "description": "The expiry warning horizon (ExpireStorePointsJob::WARN_DAYS).",
            "example": 14
          }
        }
      },
      "CompanyStoreConfigRedemption": {
        "type": "object",
        "description": "The rules a redemption is subject to before it becomes an order, mirroring Store::CheckoutService's own readings so a client can disclose a hold or a cap BEFORE the user commits. Every figure is null when off, never 0.",
        "properties": {
          "approval_threshold_points": {
            "type": "integer",
            "nullable": true,
            "description": "The points figure AT OR ABOVE which a redemption is HELD for approval instead of spending immediately (checkout compares `>=`). The LOWEST enabled of the admin and manager tiers, since that is the one that actually holds; a tier set to 0 is off. null = no tier enabled."
          },
          "monthly_points_cap": {
            "type": "integer",
            "nullable": true,
            "description": "Per-user calendar-month ceiling on redeemed points. null = unlimited."
          },
          "monthly_points_remaining": {
            "type": "integer",
            "nullable": true,
            "description": "What is left of that ceiling for THIS caller this month, counting the same set checkout counts (points and mixed orders, cancelled and refunded excluded). Floored at 0. null when there is no cap — and the query behind it is then skipped entirely."
          },
          "velocity_window_hours": {
            "type": "integer",
            "description": "The short-window fraud brake's window. Reports the checkout service's own 24h fallback when unset.",
            "example": 24
          },
          "velocity_max_orders": {
            "type": "integer",
            "nullable": true,
            "description": "Redemptions allowed inside the window. A breach does NOT block — it forces the approval hold. null = off."
          },
          "velocity_max_points": {
            "type": "integer",
            "nullable": true,
            "description": "Points allowed inside the window. Same non-blocking hold semantics. null = off."
          }
        }
      },
      "CompanyStoreOrdersViewer": {
        "type": "object",
        "description": "Who is asking. Shared verbatim with every other Company Store endpoint.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "image": {
            "type": "string",
            "nullable": true
          },
          "is_manager": {
            "type": "boolean",
            "description": "Does this viewer have a redemption approval queue — the one thing that separates the design's Manager persona from its Employee one. It does NOT widen the order list; the approval queue is its own surface."
          },
          "is_store_admin": {
            "type": "boolean",
            "description": "A business admin/owner or a Company Store app-admin."
          }
        }
      },
      "CompanyStoreOrdersFeatures": {
        "type": "object",
        "description": "The TENANT switches, shared verbatim with the other Company Store endpoints. Present on the orders payload because the reorder affordance depends on them — an item is only reorderable through a payment path the tenant actually enabled.",
        "properties": {
          "points_redemption_enabled": {
            "type": "boolean"
          },
          "cash_purchases_enabled": {
            "type": "boolean"
          },
          "mixed_payments_enabled": {
            "type": "boolean"
          },
          "recognition_integration_enabled": {
            "type": "boolean"
          },
          "regions_enabled": {
            "type": "boolean"
          },
          "points_per_dollar": {
            "type": "integer"
          }
        }
      },
      "CompanyStoreOrderStatusCounts": {
        "type": "object",
        "description": "Order counts by status from ONE grouped query. Every key is always present (0 when empty), so a filter bar renders a stable set of pills instead of reading an absent key as zero. Honours `search`, not `status`.",
        "required": [
          "all",
          "pending_approval",
          "pending",
          "processing",
          "fulfilled",
          "cancelled",
          "refunded"
        ],
        "properties": {
          "all": {
            "type": "integer",
            "description": "Every order in the scope, in every status — the \"All\" pill.",
            "example": 14
          },
          "pending_approval": {
            "type": "integer",
            "example": 1
          },
          "pending": {
            "type": "integer",
            "example": 2
          },
          "processing": {
            "type": "integer",
            "example": 3
          },
          "fulfilled": {
            "type": "integer",
            "example": 6
          },
          "cancelled": {
            "type": "integer",
            "example": 2
          },
          "refunded": {
            "type": "integer",
            "example": 0
          }
        }
      },
      "CompanyStoreOrderStatusFilter": {
        "type": "object",
        "description": "One pill in the filter row.",
        "properties": {
          "value": {
            "type": "string",
            "nullable": true,
            "description": "The `status` query value this pill applies. **null** is the All pill."
          },
          "label": {
            "type": "string",
            "description": "Display label. `pending_approval` reads \"Awaiting Approval\".",
            "example": "Awaiting Approval"
          },
          "count": {
            "type": "integer",
            "description": "The same figure `counts` reports for this status."
          },
          "selected": {
            "type": "boolean",
            "description": "True for the pill matching the applied filter (the All pill when none is applied)."
          },
          "visible": {
            "type": "boolean",
            "description": "Whether the web chip row would show this pill. False only for `pending_approval` / `refunded` while they have no rows and are not selected."
          }
        }
      },
      "CompanyStoreOrderListRow": {
        "type": "object",
        "description": "One row in the order list — the shared `CompanyStoreOrderRow` card plus the three fields the orders screen adds, and `employee` in the `all` scope.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "order_number": {
            "type": "string",
            "example": "ORD-20260819-ABC123"
          },
          "item": {
            "type": "object",
            "nullable": true,
            "description": "null when the item row has since been deleted — the row still has to render, so use `item_name` for the label.",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "image_url": {
                "type": "string",
                "nullable": true,
                "description": "Absolute URL."
              }
            }
          },
          "item_name": {
            "type": "string",
            "description": "Always populated (\"Item\" for a deleted item row)."
          },
          "quantity": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending_approval",
              "pending",
              "processing",
              "fulfilled",
              "cancelled",
              "refunded"
            ]
          },
          "status_label": {
            "type": "string",
            "description": "Canonical display label — \"Awaiting Approval\" for `pending_approval`."
          },
          "points_spent": {
            "type": "integer"
          },
          "cash_amount": {
            "type": "number",
            "format": "float"
          },
          "total_label": {
            "type": "string",
            "description": "Pre-formatted money summary, e.g. \"1200 pts\" or \"1200 pts + $5.00\"."
          },
          "payment_type": {
            "type": "string",
            "enum": [
              "points",
              "cash",
              "mixed"
            ]
          },
          "variant_display": {
            "type": "string",
            "nullable": true,
            "description": "Selected variants as one line, e.g. \"Size: L, Color: Black\". null when the item has none."
          },
          "has_fulfillment_error": {
            "type": "boolean",
            "description": "True only while a FAILED order is still `pending`. A resolved order reports false, so never treat this as sticky."
          },
          "placed_at": {
            "type": "string",
            "format": "date-time"
          },
          "url": {
            "type": "string",
            "description": "Absolute URL of the web order page."
          },
          "employee": {
            "$ref": "#/components/schemas/CompanyStorePerson"
          }
        }
      },
      "CompanyStorePerson": {
        "type": "object",
        "nullable": true,
        "description": "Whose order this is. Present on a list row ONLY in the `all` scope (in `mine` every row is the caller's, so naming them per row is noise), and always present on the detail payload.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "title": {
            "type": "string",
            "nullable": true
          },
          "image": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "CompanyStoreOrdersPageMeta": {
        "type": "object",
        "description": "The page envelope every paginated Company Store endpoint answers with — one shape, produced by one shared helper (`Api::V1::CompanyStore::BaseController#pagination_meta`). Structurally identical to `CompanyStorePageMeta` in the sibling catalog spec file; the two should collapse into one schema once both surfaces have landed.",
        "properties": {
          "current_page": {
            "type": "integer"
          },
          "per_page": {
            "type": "integer"
          },
          "total_count": {
            "type": "integer",
            "description": "Rows in the FILTERED pool (status + search), not on this page."
          },
          "total_pages": {
            "type": "integer"
          },
          "has_next_page": {
            "type": "boolean"
          },
          "has_prev_page": {
            "type": "boolean"
          }
        }
      },
      "CompanyStoreOrderTimelineStep": {
        "type": "object",
        "description": "One step of the derived lifecycle timeline.",
        "properties": {
          "key": {
            "type": "string",
            "enum": [
              "placed",
              "awaiting_approval",
              "processing",
              "delivered",
              "cancelled",
              "refunded"
            ]
          },
          "label": {
            "type": "string",
            "description": "Display label. The delivery step reads \"Delivered\" for a digital reward and \"On its way\" for anything shipped, so a gift card never promises a truck.",
            "example": "On its way"
          },
          "detail": {
            "type": "string",
            "nullable": true,
            "description": "The sentence beside the label — the reassurance copy, or the delivery estimate (\"Est. 5 business days after it ships\", or \"Delivery estimate not available\" when the item carries no lead time)."
          },
          "state": {
            "type": "string",
            "enum": [
              "done",
              "current",
              "upcoming",
              "cancelled",
              "refunded"
            ]
          },
          "icon": {
            "type": "string",
            "description": "Font Awesome class the web page paints; a native client may map its own.",
            "example": "fas fa-truck"
          },
          "at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the step happened. null for a step that hasn't happened yet, and for a legacy terminal row that never got a timestamp."
          }
        }
      },
      "CompanyStoreOrderDetail": {
        "type": "object",
        "description": "One order in full. Carries every `CompanyStoreOrderListRow` field plus the blocks below.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "order_number": {
            "type": "string"
          },
          "item": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "image_url": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "item_name": {
            "type": "string"
          },
          "quantity": {
            "type": "integer"
          },
          "status": {
            "type": "string"
          },
          "status_label": {
            "type": "string"
          },
          "points_spent": {
            "type": "integer"
          },
          "cash_amount": {
            "type": "number",
            "format": "float"
          },
          "total_label": {
            "type": "string"
          },
          "payment_type": {
            "type": "string",
            "enum": [
              "points",
              "cash",
              "mixed"
            ]
          },
          "placed_at": {
            "type": "string",
            "format": "date-time"
          },
          "url": {
            "type": "string"
          },
          "viewer": {
            "$ref": "#/components/schemas/CompanyStoreOrdersViewer"
          },
          "features": {
            "$ref": "#/components/schemas/CompanyStoreOrdersFeatures"
          },
          "is_mine": {
            "type": "boolean",
            "description": "False when a store admin is viewing someone else's order. Every entry in `actions` is then false — viewing is not acting."
          },
          "employee": {
            "$ref": "#/components/schemas/CompanyStorePerson"
          },
          "variant_display": {
            "type": "string",
            "nullable": true
          },
          "selected_variants": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "The chosen variants as a map, e.g. {\"size\": \"L\"}. Empty when the item has none."
          },
          "personalization": {
            "type": "object",
            "nullable": true,
            "description": "Engraving details captured at checkout for personalizable items (recipient_name, and optionally years_of_service / engraving_line). null for everything else.",
            "additionalProperties": {
              "type": "string"
            }
          },
          "line_items": {
            "type": "array",
            "description": "Every line the order contains. A multi-item cart order has real rows; a single-item redemption gets ONE synthesised line, so a client renders the same list either way.",
            "items": {
              "type": "object",
              "properties": {
                "item_id": {
                  "type": "integer",
                  "nullable": true
                },
                "name": {
                  "type": "string"
                },
                "quantity": {
                  "type": "integer"
                },
                "points_price": {
                  "type": "integer"
                },
                "cash_price": {
                  "type": "number",
                  "format": "float"
                },
                "selected_variants": {
                  "type": "object",
                  "additionalProperties": {
                    "type": "string"
                  }
                }
              }
            }
          },
          "dates": {
            "type": "object",
            "properties": {
              "placed_at": {
                "type": "string",
                "format": "date-time"
              },
              "fulfilled_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "cancelled_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "refunded_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              }
            }
          },
          "timeline": {
            "type": "array",
            "description": "Ordered lifecycle steps — see the endpoint description.",
            "items": {
              "$ref": "#/components/schemas/CompanyStoreOrderTimelineStep"
            }
          },
          "fulfillment": {
            "type": "object",
            "description": "The reward payload — what the employee came to this screen for once the order lands. Fields are null until fulfilment provides them.",
            "properties": {
              "tracking_number": {
                "type": "string",
                "nullable": true
              },
              "carrier": {
                "type": "string",
                "nullable": true
              },
              "tracking_url": {
                "type": "string",
                "nullable": true,
                "description": "The provider's own deep link when it supplied one, otherwise built from a known carrier + number. null means render the bare number."
              },
              "gift_card_code": {
                "type": "string",
                "nullable": true
              },
              "reward_link": {
                "type": "string",
                "nullable": true
              },
              "delivery_email": {
                "type": "string",
                "nullable": true,
                "description": "Where the redemption link was sent."
              },
              "donation_receipt": {
                "type": "string",
                "nullable": true,
                "description": "Receipt number for a charitable redemption, when the admin recorded one."
              },
              "has_gift_card_link": {
                "type": "boolean",
                "description": "True when either a code or a redemption link is available."
              }
            }
          },
          "shipping_address": {
            "type": "object",
            "nullable": true,
            "description": "null for anything that doesn't ship, and for a shipped order with no address recorded. `lines` is the same formatted block the web page prints, so both surfaces render one address format.",
            "properties": {
              "lines": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "approval": {
            "type": "object",
            "nullable": true,
            "description": "Present ONLY while the redemption is actually held — that is the only time \"who has to approve this\" is a live question.",
            "properties": {
              "required_approver": {
                "type": "string",
                "enum": [
                  "manager",
                  "admin"
                ],
                "description": "The tier stamped at checkout. A legacy hold with none reads `admin`."
              },
              "manager_approval": {
                "type": "boolean"
              },
              "points_held": {
                "type": "integer",
                "description": "The points held while the decision is pending."
              }
            }
          },
          "return_request": {
            "type": "object",
            "nullable": true,
            "description": "The latest return / exchange request, so a client shows \"Return requested\" instead of re-offering the button. null when there is none.",
            "properties": {
              "id": {
                "type": "integer"
              },
              "status": {
                "type": "string"
              },
              "return_type": {
                "type": "string",
                "enum": [
                  "refund",
                  "exchange"
                ]
              },
              "reason": {
                "type": "string"
              },
              "requested_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "actions": {
            "type": "object",
            "description": "What this viewer may actually do next, resolved against the same predicates the write paths enforce — including this API's own cancel endpoint, so a control rendered from here is one whose POST is accepted. `can_cancel` is offered to both people the web offers it to (the buyer, and a store admin on anyone's order); the other three are the BUYER's own acts and are false for an admin however wide their read access.",
            "properties": {
              "can_cancel": {
                "type": "boolean",
                "description": "True when POST /company-store/orders/{order_number}/cancel would be accepted for this viewer — the very same `StoreOrder#cancel_actor_for` reading that endpoint's gate uses."
              },
              "cancel_as": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "owner",
                  "admin"
                ],
                "description": "Which cancel power this viewer holds. `owner` is the buyer's self-service cancel, `admin` a store admin acting on an order — worth distinguishing, because an admin cancelling somebody else's order deserves a different confirmation. null when they hold none."
              },
              "cancel_blocked_reason": {
                "type": "string",
                "nullable": true,
                "description": "Why cancel is unavailable while the status still looks cancellable — already dispatched to the reward provider, not the caller's order, or a cash refund needing a business administrator. null when the order can be cancelled, and when there is no cancel affordance at all (a terminal order has the return path instead)."
              },
              "can_return": {
                "type": "boolean",
                "description": "A fulfilled PHYSICAL order with no open/approved return."
              },
              "can_report_problem": {
                "type": "boolean",
                "description": "A fulfilled DIGITAL order — not physically returnable, but it can be reported as a dud."
              },
              "can_reorder": {
                "type": "boolean",
                "description": "True only when the item still exists, is available, its category toggle is on, and at least one enabled payment path applies — the same four-part test the web \"Order Again\" button uses, so the deep link is never doomed."
              },
              "reorder_url": {
                "type": "string",
                "nullable": true,
                "description": "Absolute checkout URL carrying the original quantity and variants."
              }
            }
          },
          "admin": {
            "type": "object",
            "description": "Store admins only — absent entirely for everyone else. The web shows these on the admin order page; an employee gets reassurance copy, not a connector error string.",
            "properties": {
              "provider_order_id": {
                "type": "string",
                "nullable": true
              },
              "provider_status": {
                "type": "string",
                "nullable": true
              },
              "funding_source": {
                "type": "string",
                "enum": [
                  "tenant_byo",
                  "platform_managed"
                ]
              },
              "fulfillment_error": {
                "type": "string",
                "nullable": true,
                "description": "The connector's own failure message."
              },
              "can_retry_fulfillment": {
                "type": "boolean"
              },
              "admin_url": {
                "type": "string",
                "description": "Absolute URL of the web admin order page, where the decisions live."
              }
            }
          }
        }
      },
      "CompanyStoreOrderCancelResponse": {
        "type": "object",
        "description": "The receipt for the cancellation, plus the whole order re-rendered so the client needs no follow-up GET.",
        "properties": {
          "order": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CompanyStoreOrderDetail"
              }
            ],
            "description": "The order as it now stands — status `cancelled`, a `cancelled` terminal step on the timeline, and `actions.can_cancel` withdrawn."
          },
          "cancellation": {
            "type": "object",
            "description": "What was just done, and to what.",
            "properties": {
              "order_number": {
                "type": "string",
                "example": "ORD-20260819-ABC123"
              },
              "status": {
                "type": "string",
                "example": "cancelled"
              },
              "status_label": {
                "type": "string",
                "example": "Cancelled"
              },
              "cancelled_at": {
                "type": "string",
                "format": "date-time"
              },
              "cancelled_by": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/CompanyStorePerson"
                  }
                ],
                "description": "Whoever took the action — the buyer, or the admin who cancelled for them."
              },
              "actor": {
                "type": "string",
                "enum": [
                  "owner",
                  "admin"
                ],
                "description": "Which power was exercised. `owner` is the buyer's self-service cancel; `admin` a store admin acting on an order."
              },
              "reason": {
                "type": "string",
                "nullable": true,
                "description": "The `reason` sent with the request, as recorded. null when none was sent."
              },
              "points_refunded": {
                "type": "integer",
                "description": "Points returned to the employee's wallet — the PRE-write reading, so it still reports what the order held.",
                "example": 900
              },
              "card_refund_pending": {
                "type": "boolean",
                "description": "True when a captured card charge is being reversed. The Stripe refund runs asynchronously, so this means \"a refund is on its way\", never \"the money is back\"."
              },
              "inventory_restored": {
                "type": "integer",
                "description": "Units actually returned to the item's stock — the order quantity, or 0 for an item with unlimited inventory, which tracks none.",
                "example": 1
              }
            }
          },
          "message": {
            "type": "string",
            "description": "Ready-to-show confirmation copy, phrased for whoever asked: the buyer is told about their own balance, an admin about the employee's. Built from the same three facts the web self-cancel flash is built from.",
            "example": "Order #ORD-20260819-ABC123 was cancelled. 900 points were returned to your balance."
          },
          "unread_notification_count": {
            "type": "integer",
            "description": "Native app badge count (the shared api/v1 envelope)."
          }
        }
      },
      "CompanyStoreOrderCancelError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "access_denied",
                  "store_disabled",
                  "forbidden",
                  "cash_reversal_forbidden",
                  "not_cancellable",
                  "cancel_failed",
                  "not_found"
                ]
              },
              "message": {
                "type": "string",
                "description": "Ready-to-show copy, and the SAME sentence the web surfaces show for the same refusal — an admin is never told to \"contact your store admin\", because they are one.",
                "example": "This order has already been sent to the reward provider, so it can't be cancelled here."
              }
            }
          }
        }
      },
      "CompanyStorePointsViewer": {
        "type": "object",
        "description": "Who is asking. Shared verbatim with every other Company Store endpoint. Reported for consistency and so a client can offer the admin surfaces — neither flag changes anything in this payload, which is always the caller's own wallet.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "image": {
            "type": "string",
            "nullable": true
          },
          "is_manager": {
            "type": "boolean",
            "description": "Does this viewer have a redemption approval queue. It does NOT widen the points payload; the approval queue is its own surface."
          },
          "is_store_admin": {
            "type": "boolean",
            "description": "A business admin/owner or a Company Store app-admin. It does NOT let this endpoint read someone else's wallet — that is the admin balances surface."
          }
        }
      },
      "CompanyStorePointsFeatures": {
        "type": "object",
        "description": "The TENANT switches. The first six are shared verbatim with the other Company Store endpoints; `points_expiry_enabled` is specific to this screen.",
        "properties": {
          "points_redemption_enabled": {
            "type": "boolean"
          },
          "cash_purchases_enabled": {
            "type": "boolean"
          },
          "mixed_payments_enabled": {
            "type": "boolean"
          },
          "recognition_integration_enabled": {
            "type": "boolean"
          },
          "regions_enabled": {
            "type": "boolean"
          },
          "points_per_dollar": {
            "type": "integer"
          },
          "points_expiry_enabled": {
            "type": "boolean",
            "description": "Whether the tenant expires points at all (`points_expiry_months` > 0). This is what distinguishes `balance.expiring_points: 0` meaning \"nothing is close\" from \"this tenant does not expire points\" — the second should render no expiry banner or countdown."
          }
        }
      },
      "CompanyStorePointsBalance": {
        "type": "object",
        "description": "The wallet hero. The first six fields are the shared balance block every Company Store endpoint reports; the expiry pair is this screen's (and the dashboard's) addition.",
        "required": [
          "points_balance",
          "pending_points",
          "lifetime_points_earned",
          "lifetime_points_spent",
          "expiring_points",
          "expiring_within_days"
        ],
        "properties": {
          "points_balance": {
            "type": "integer",
            "description": "Spendable points right now. The \"Available Points\" tile.",
            "example": 2450
          },
          "pending_points": {
            "type": "integer",
            "description": "Earned but not yet credited — a recognition has landed and is awaiting release. Not spendable, so never add it to `points_balance` when deciding affordability.",
            "example": 150
          },
          "lifetime_points_earned": {
            "type": "integer",
            "example": 8200
          },
          "lifetime_points_spent": {
            "type": "integer",
            "description": "Lifetime points redeemed. Expiries are NOT counted here — breakage is not a redemption.",
            "example": 5750
          },
          "last_earned_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "last_spent_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "expiring_points": {
            "type": "integer",
            "description": "What NEWLY expires within `expiring_within_days` — the figure the web banner claims and the expiry warning notification sends. NOT the total currently-expirable pool. Always 0 when `features.points_expiry_enabled` is false.",
            "example": 300
          },
          "expiring_within_days": {
            "type": "integer",
            "description": "The warn horizon `expiring_points` was computed over.",
            "example": 14
          }
        }
      },
      "CompanyStorePointsActivity": {
        "type": "object",
        "description": "The activity roll-up over the last `period_days`, from ONE grouped query.",
        "required": [
          "period_days",
          "earned",
          "spent",
          "adjustments",
          "transaction_count"
        ],
        "properties": {
          "period_days": {
            "type": "integer",
            "description": "The window actually computed — 90 on this screen, deliberately NOT the dashboard widget's 30. Render the header from this rather than hardcoding it.",
            "example": 90
          },
          "earned": {
            "type": "integer",
            "description": "Sum of credits in the window (positive).",
            "example": 1200
          },
          "spent": {
            "type": "integer",
            "description": "Sum of debits in the window, as a POSITIVE magnitude.",
            "example": 800
          },
          "adjustments": {
            "type": "integer",
            "description": "Net of admin adjustments in the window — SIGNED, because an adjustment can go either way, so this may be negative.",
            "example": -50
          },
          "transaction_count": {
            "type": "integer",
            "description": "Every row in the window, of any type. Expiry and any future type count here without landing in `earned` or `spent`, so this is not necessarily the number of credits plus debits.",
            "example": 18
          }
        }
      },
      "CompanyStorePointsTrend": {
        "type": "object",
        "description": "The 6-month earned-vs-spent trend the web page charts.",
        "required": [
          "months",
          "has_activity"
        ],
        "properties": {
          "months": {
            "type": "array",
            "description": "Always 6 entries, oldest first, including zero-activity months so a chart renders at a stable width.",
            "items": {
              "$ref": "#/components/schemas/CompanyStorePointsTrendMonth"
            }
          },
          "has_activity": {
            "type": "boolean",
            "description": "True when any month has non-zero earned or spent. What the web page gates the whole card on — a flat all-zero chart is worse than no chart."
          }
        }
      },
      "CompanyStorePointsTrendMonth": {
        "type": "object",
        "properties": {
          "month": {
            "type": "string",
            "format": "date",
            "description": "First day of the month, for sorting and locale-aware formatting.",
            "example": "2026-08-01"
          },
          "label": {
            "type": "string",
            "description": "Server-rendered short label, matching the web chart's axis.",
            "example": "Aug 2026"
          },
          "earned": {
            "type": "integer",
            "description": "Credits in the month.",
            "example": 400
          },
          "spent": {
            "type": "integer",
            "description": "Debits in the month, as a POSITIVE magnitude. Adjustments and expiries appear in NEITHER series — this is the earn-vs-redeem picture, not a net-change chart.",
            "example": 250
          }
        }
      },
      "CompanyStorePointsTypeCounts": {
        "type": "object",
        "description": "ALL-TIME counts by transaction type from ONE grouped query. Every key is always present (0 when empty), so a filter row renders a stable set of pills instead of reading an absent key as zero. Deliberately NOT narrowed by the active `type` — see the endpoint description.",
        "required": [
          "all",
          "credit",
          "debit",
          "adjustment",
          "expiry"
        ],
        "properties": {
          "all": {
            "type": "integer",
            "description": "Every row in the caller's history, of every type — the \"All\" pill. This is the WHOLE history even when the list is filtered; the filtered depth is `meta.total_count`.",
            "example": 42
          },
          "credit": {
            "type": "integer",
            "example": 28
          },
          "debit": {
            "type": "integer",
            "example": 11
          },
          "adjustment": {
            "type": "integer",
            "example": 3
          },
          "expiry": {
            "type": "integer",
            "example": 0
          }
        }
      },
      "CompanyStorePointsTypeFilter": {
        "type": "object",
        "description": "One pill in the filter row.",
        "properties": {
          "value": {
            "type": "string",
            "nullable": true,
            "description": "The `type` query value this pill applies. **null** is the All pill.",
            "enum": [
              "credit",
              "debit",
              "adjustment",
              "expiry",
              null
            ]
          },
          "label": {
            "type": "string",
            "description": "Display label, matching the web chip and row badge (\"All\", \"Credit\", \"Debit\", \"Adjustment\", \"Expiry\").",
            "example": "Credit"
          },
          "count": {
            "type": "integer",
            "description": "The same number `counts` reports for this type.",
            "example": 28
          },
          "selected": {
            "type": "boolean",
            "description": "Whether this pill is the filter currently in force."
          },
          "visible": {
            "type": "boolean",
            "description": "Whether the web would render this pill. False only for `expiry` while its count is 0 and it is not selected. A client may ignore this and render all five."
          }
        }
      },
      "CompanyStorePointsTaxStatement": {
        "type": "object",
        "description": "The self-service tax statement link, mirroring the web header button.",
        "required": [
          "available"
        ],
        "properties": {
          "available": {
            "type": "boolean",
            "description": "True only when the viewer has taxable redemptions in a year the statement page offers. False means do not render the link — it would dead-end on an empty statement."
          },
          "year": {
            "type": "integer",
            "nullable": true,
            "description": "The most recent offered year that actually HAS rows — not necessarily the current year. In Jan–Apr they usually differ, so link to this value rather than to \"this year\".",
            "example": 2025
          },
          "url": {
            "type": "string",
            "nullable": true,
            "description": "Absolute URL to the statement for `year`. null when unavailable."
          }
        }
      },
      "CompanyStorePointsTransaction": {
        "type": "object",
        "description": "One row of the points ledger.",
        "required": [
          "id",
          "transaction_type",
          "type_label",
          "amount",
          "display_amount",
          "absolute_amount",
          "positive",
          "balance_after",
          "description",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "transaction_type": {
            "type": "string",
            "enum": [
              "credit",
              "debit",
              "adjustment",
              "expiry"
            ]
          },
          "type_label": {
            "type": "string",
            "description": "Titleized label, matching the web badge.",
            "example": "Credit"
          },
          "amount": {
            "type": "integer",
            "description": "SIGNED. Positive for credits, negative for debits and expiries, either for adjustments.",
            "example": -500
          },
          "display_amount": {
            "type": "string",
            "description": "Pre-formatted signed string, as the web prints it.",
            "example": "-500"
          },
          "absolute_amount": {
            "type": "integer",
            "description": "Magnitude, sign discarded.",
            "example": 500
          },
          "positive": {
            "type": "boolean",
            "description": "The direction of the entry. Report this rather than inferring from the type — an `adjustment` can go either way, which is why the web colours the amount on the sign."
          },
          "balance_after": {
            "type": "integer",
            "description": "The caller's spendable balance immediately after this entry.",
            "example": 1950
          },
          "description": {
            "type": "string",
            "description": "What happened, as the web row prints it. Falls back to a source-derived phrase (\"Recognition received\", \"Store redemption\") for rows stored without one.",
            "example": "Redeemed Branded Hoodie"
          },
          "notes": {
            "type": "string",
            "nullable": true,
            "description": "The admin's stated reason on an adjustment. The employee already receives it in the adjustment notification. null on every other type."
          },
          "source": {
            "type": "object",
            "nullable": true,
            "description": "A REFERENCE to the record that caused this entry, not a resolved label — resolving it would cost a polymorphic load per row, and the web rows print the type and nothing more. null for entries with no source (expiries, most adjustments). Fetch the record by reference if its name is needed.",
            "properties": {
              "type": {
                "type": "string",
                "description": "Model name.",
                "example": "StoreOrder"
              },
              "id": {
                "type": "integer",
                "example": 8123
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CompanyStorePointsPageMeta": {
        "type": "object",
        "description": "The page envelope, identical in shape to every other paginated Company Store endpoint. `total_count` is the depth of the FILTERED list — for the whole history regardless of filter, read `counts.all`.",
        "required": [
          "current_page",
          "per_page",
          "total_count",
          "total_pages",
          "has_next_page",
          "has_prev_page"
        ],
        "properties": {
          "current_page": {
            "type": "integer",
            "example": 1
          },
          "per_page": {
            "type": "integer",
            "description": "The clamped page size in force.",
            "example": 20
          },
          "total_count": {
            "type": "integer",
            "description": "Rows matching the active `type` filter.",
            "example": 11
          },
          "total_pages": {
            "type": "integer",
            "example": 1
          },
          "has_next_page": {
            "type": "boolean"
          },
          "has_prev_page": {
            "type": "boolean"
          }
        }
      },
      "CompanyStoreShippingCountriesResponse": {
        "type": "object",
        "properties": {
          "shipping_countries": {
            "type": "object",
            "properties": {
              "countries": {
                "type": "array",
                "description": "The countries a physical order can be shipped to, in display order. Never empty. Render whatever length this holds rather than assuming one entry.",
                "items": {
                  "$ref": "#/components/schemas/CompanyStoreShippingCountry"
                }
              },
              "default_country": {
                "type": "string",
                "description": "The country to select when the caller has expressed no preference — always the `value` of one of the rows above, so it can be assigned to the field directly.",
                "example": "United States"
              },
              "prefill": {
                "$ref": "#/components/schemas/CompanyStoreShippingPrefill"
              }
            }
          },
          "unread_notification_count": {
            "type": "integer",
            "description": "Piggybacked on every response in this API."
          },
          "_meta": {
            "type": "object",
            "description": "Piggybacked response metadata."
          }
        }
      },
      "CompanyStoreShippingCountry": {
        "type": "object",
        "properties": {
          "value": {
            "type": "string",
            "description": "**Submit this** as `shipping_address[country]`. The display NAME, not the ISO code — matching the web form and what lands in `order.shipping_address[\"country\"]`.",
            "example": "United States"
          },
          "code": {
            "type": "string",
            "description": "ISO 3166-1 alpha-2. Display and validation only — do NOT submit it as the country field; the write would accept it and fulfillment would then fail on it.",
            "example": "US"
          },
          "label": {
            "type": "string",
            "description": "What to show in the dropdown.",
            "example": "United States"
          },
          "states": {
            "type": "array",
            "description": "The country's states/provinces. `[]` for a country we hold no list for, which means render a free-text field.",
            "items": {
              "$ref": "#/components/schemas/CompanyStoreShippingState"
            }
          }
        }
      },
      "CompanyStoreShippingState": {
        "type": "object",
        "properties": {
          "value": {
            "type": "string",
            "description": "**Submit this** as `shipping_address[state]`. The 2-letter code — the opposite shape to the country field, because the Printful fulfillment path reads it as a `state_code`.",
            "example": "CA"
          },
          "label": {
            "type": "string",
            "description": "What to show in the dropdown.",
            "example": "California"
          }
        }
      },
      "CompanyStoreShippingPrefill": {
        "type": "object",
        "description": "The caller's own profile values, resolved into the shapes the two fields submit. The only part of this payload that varies per user.",
        "properties": {
          "country": {
            "type": "string",
            "description": "Always one of the offered `value`s, falling back to `default_country` when the profile holds none or holds one we do not ship to. A profile country stored as an ISO code is matched and returned as the NAME.",
            "example": "United States"
          },
          "state": {
            "type": "string",
            "nullable": true,
            "description": "The profile state normalised to its 2-letter code, accepting a name, a code or a lowercase code on the way in. **Null** when the profile holds no state or one that resolves to no known code — leave the dropdown unselected rather than guessing.",
            "example": "CA"
          },
          "states_required": {
            "type": "boolean",
            "description": "Whether the prefilled country has a state list at all. `false` means an empty `states` array is expected and a free-text field is correct — it distinguishes \"no list exists\" from \"the list failed to load\".",
            "example": true
          }
        }
      },
      "CompanyStoreShippingCountriesError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "insufficient_permissions",
                  "access_denied",
                  "store_disabled"
                ]
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "CompanyStoreWatchlistResponse": {
        "type": "object",
        "properties": {
          "watchlist": {
            "$ref": "#/components/schemas/CompanyStoreWatchlistState"
          },
          "unread_notification_count": {
            "type": "integer",
            "description": "The caller's unread in-app notification count, piggybacked onto every response in this API for native badge management.",
            "example": 3
          }
        }
      },
      "CompanyStoreWatchlistState": {
        "type": "object",
        "description": "The watch state AFTER the write — read back from the database, never assumed.",
        "properties": {
          "item_id": {
            "type": "integer",
            "example": 412
          },
          "item_name": {
            "type": "string",
            "description": "So a client can compose its own copy without holding the item row.",
            "example": "Company Logo Hoodie"
          },
          "kind": {
            "type": "string",
            "enum": [
              "wishlist",
              "restock"
            ],
            "description": "The kind this call acted on (the default `wishlist` when none was sent)."
          },
          "watching": {
            "type": "boolean",
            "description": "Is the caller now watching this item for `kind`? `true` on every successful ADD (including an idempotent repeat), `false` on every successful REMOVE. It describes what is TRUE now, not what this call did — see `changed`.",
            "example": true
          },
          "changed": {
            "type": "boolean",
            "description": "Did THIS call move anything? `false` on an idempotent repeat — an add of an item already watched, or a remove of one that wasn't. Suppress a duplicate toast on `false`; do not treat it as a failure.",
            "example": true
          },
          "wishlisted": {
            "type": "boolean",
            "description": "Is the item on the caller's saved items? Named exactly as the catalog card's own field, so a cached card is patched field-for-field. Resolved in the SAME query as `restock_watch`."
          },
          "restock_watch": {
            "type": "boolean",
            "description": "Does the caller hold a back-in-stock alert for this item? Named as `GET /company-store/catalog/{id}` reports it. Remember it is one-shot — it disappears once the notification is sent."
          },
          "wishlist_total": {
            "type": "integer",
            "description": "How many saved items the caller now has — `Store::DashboardStats#wishlist_total`, the same figure `GET /company-store/dashboard` reports as `saved_items.total`, so a badge updates from the response that changed it. It is the SAVED-ITEMS scope: watches whose item has since been discontinued, unpublished, or restricted to a group this caller isn't in are not counted, exactly as the dashboard grid omits them. Sent on both verbs and both kinds; a `restock` write leaves it unchanged.",
            "example": 7
          },
          "message": {
            "type": "string",
            "description": "Ready-to-display confirmation, byte-identical to the web toggle's (`StoreItemWatch.state_message`) — a heart flipping 16px is not enough confirmation that anything was saved. One of four strings: \"Added X to your wishlist.\" / \"Removed X from your wishlist.\" / \"We'll notify you when X is back in stock.\" / \"Back-in-stock alert for X turned off.\"",
            "example": "Added Company Logo Hoodie to your wishlist."
          }
        }
      },
      "CompanyStoreWatchlistError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "invalid_item_id",
                  "access_denied",
                  "store_disabled",
                  "region_unavailable",
                  "region_restricted",
                  "not_found",
                  "invalid_kind",
                  "restock_not_applicable"
                ]
              },
              "message": {
                "type": "string",
                "description": "Human-readable, safe to display."
              },
              "details": {
                "type": "object",
                "nullable": true,
                "description": "Present on the three refusals that can name what was wrong: `{ \"item_id\": \"<what was sent>\" }` for `invalid_item_id`, `{ \"kind\": \"<what was sent>\" }` for `invalid_kind`, and `{ \"status\": \"<the item's status>\" }` for `restock_not_applicable`."
              }
            }
          }
        }
      },
      "FrontlineBlockedItem": {
        "type": "object",
        "description": "The obligation after the flag — the same shape the item-action endpoints return.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 18
          },
          "campaign_id": {
            "type": "integer",
            "example": 42
          },
          "title": {
            "type": "string",
            "example": "Reset the seasonal endcap"
          },
          "location": {
            "type": "string",
            "nullable": true,
            "example": "Fourth Street"
          },
          "location_id": {
            "type": "integer",
            "nullable": true,
            "example": 12
          },
          "status": {
            "type": "string",
            "description": "Unchanged by the flag — a blocker is an annotation, not a transition.",
            "example": "claimed"
          },
          "priority": {
            "type": "string",
            "nullable": true,
            "example": "high"
          },
          "due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "overdue": {
            "type": "boolean",
            "example": false
          },
          "handoff_count": {
            "type": "integer",
            "example": 0
          },
          "requires_photo": {
            "type": "boolean",
            "nullable": true,
            "example": true
          },
          "requires_signature": {
            "type": "boolean",
            "nullable": true,
            "example": false
          },
          "blocked": {
            "type": "boolean",
            "description": "True once flagged — the key that tells a client the flag took.",
            "example": true
          },
          "blocked_reason": {
            "type": "string",
            "nullable": true,
            "description": "The structured cause that was recorded.",
            "example": "no_stock"
          },
          "blocked_note": {
            "type": "string",
            "nullable": true,
            "description": "The free-text detail, if any.",
            "example": "Fridge 2 alarm is sounding, panel reads ERR."
          },
          "blocked_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "FrontlineBlockError": {
        "type": "object",
        "description": "The standard error envelope for a refused flag.",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "`invalid_reason`, `not_assigned`, `already_done` or `in_review`.",
                "example": "invalid_reason"
              },
              "message": {
                "type": "string",
                "example": "Pick what's blocking this work."
              }
            }
          }
        }
      },
      "FrontlineChecklistItemResult": {
        "type": "object",
        "properties": {
          "item": {
            "type": "object",
            "description": "The obligation after the tick — the same shape the item-action endpoints return.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 18
              },
              "campaign_id": {
                "type": "integer",
                "example": 42
              },
              "title": {
                "type": "string",
                "example": "Reset the seasonal endcap"
              },
              "status": {
                "type": "string",
                "example": "in_progress"
              },
              "location": {
                "type": "string",
                "nullable": true,
                "example": "Fourth Street"
              },
              "location_id": {
                "type": "integer",
                "nullable": true,
                "example": 12
              }
            }
          },
          "checklist_item_id": {
            "type": "integer",
            "description": "The step that was just updated.",
            "example": 91
          },
          "completed": {
            "type": "boolean",
            "description": "The step's state after this call.",
            "example": true
          },
          "all_completed": {
            "type": "boolean",
            "description": "True when every step on the task is now done — the cue to offer Complete.",
            "example": false
          },
          "task_status": {
            "type": "string",
            "description": "The minted checklist Task's status after the tick.",
            "example": "in_progress"
          },
          "checklist": {
            "type": "object",
            "description": "The full, freshly-read checklist — identical to the `checklist` block on GET /items/{id}.",
            "properties": {
              "total": {
                "type": "integer",
                "example": 3
              },
              "completed": {
                "type": "integer",
                "example": 1
              },
              "percentage": {
                "type": "number",
                "format": "float",
                "example": 33.3
              },
              "items": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer",
                      "example": 91
                    },
                    "title": {
                      "type": "string",
                      "example": "Face and date the shelf"
                    },
                    "description": {
                      "type": "string",
                      "nullable": true
                    },
                    "sort_order": {
                      "type": "integer",
                      "example": 0
                    },
                    "completed": {
                      "type": "boolean",
                      "example": true
                    },
                    "completed_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "completed_by": {
                      "type": "string",
                      "nullable": true,
                      "example": "Dana Holder"
                    },
                    "requires_photo": {
                      "type": "boolean",
                      "example": false
                    },
                    "requires_notes": {
                      "type": "boolean",
                      "example": false
                    }
                  }
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "nullable": true,
            "description": "Present only when the step ticked but the parent Task could not auto-complete.",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "FrontlineChecklistItemError": {
        "type": "object",
        "description": "The standard error envelope for a refused tick.",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "`no_checklist`, `task_finished`, `requires_notes`, `requires_photo`, `photo_failed` or `save_failed`.",
                "example": "requires_photo"
              },
              "message": {
                "type": "string",
                "example": "This checklist item requires a photo to be marked as complete."
              }
            }
          }
        }
      },
      "ClaimedItem": {
        "type": "object",
        "description": "The item after a successful claim — the same shape the item-action endpoints return.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 90123
          },
          "campaign_id": {
            "type": "integer",
            "example": 42
          },
          "title": {
            "type": "string",
            "example": "Reset the seasonal endcap"
          },
          "location": {
            "type": "string",
            "nullable": true,
            "example": "Store 412 — Riverside"
          },
          "location_id": {
            "type": "integer",
            "nullable": true,
            "example": 3369
          },
          "status": {
            "type": "string",
            "description": "`claimed` after a successful claim.",
            "example": "claimed"
          },
          "priority": {
            "type": "string",
            "nullable": true,
            "example": "high"
          },
          "due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Unchanged by the claim — the obligation still falls due when it did."
          },
          "overdue": {
            "type": "boolean",
            "example": false
          },
          "handoff_count": {
            "type": "integer",
            "example": 0
          },
          "claimable": {
            "type": "boolean",
            "description": "Always `false` on a claimed item — it is no longer in the pool.",
            "example": false
          },
          "requires_photo": {
            "type": "boolean",
            "example": true
          },
          "requires_signature": {
            "type": "boolean",
            "example": false
          },
          "blocked": {
            "type": "boolean",
            "example": false
          },
          "assignment_explanation": {
            "type": "object",
            "nullable": true,
            "description": "Why the caller now holds this work (recorded from the claim that just happened)."
          }
        }
      },
      "ClaimError": {
        "type": "object",
        "description": "The standard error envelope for a refused claim.",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "`already_assigned`, `not_claimable`, or a critical-cap code.",
                "example": "already_assigned"
              },
              "message": {
                "type": "string",
                "example": "Someone else just claimed this work."
              }
            }
          }
        }
      },
      "CompleteItem": {
        "type": "object",
        "description": "The item after completion — the same shape the other item-action endpoints return.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 90123
          },
          "campaign_id": {
            "type": "integer",
            "example": 42
          },
          "title": {
            "type": "string",
            "example": "Reset the seasonal endcap"
          },
          "location": {
            "type": "string",
            "nullable": true,
            "example": "Store 412 — Riverside"
          },
          "location_id": {
            "type": "integer",
            "nullable": true,
            "example": 3369
          },
          "status": {
            "type": "string",
            "description": "`done` when finished, or `submitted` when the campaign requires review.",
            "example": "done"
          },
          "priority": {
            "type": "string",
            "nullable": true,
            "example": "high"
          },
          "due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "overdue": {
            "type": "boolean",
            "example": false
          },
          "handoff_count": {
            "type": "integer",
            "example": 0
          },
          "claimable": {
            "type": "boolean",
            "example": false
          },
          "requires_photo": {
            "type": "boolean",
            "example": true
          },
          "requires_signature": {
            "type": "boolean",
            "example": false
          }
        }
      },
      "CompleteError": {
        "type": "object",
        "description": "The standard error envelope for a refused completion.",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "`proof_required`, `already_done`, `missed`, `in_review`, `not_assigned`, `off_shift`, `off_site`, `location_required`, `surface_disabled`, `insufficient_permissions` or `not_found`.",
                "example": "proof_required"
              },
              "message": {
                "type": "string",
                "example": "Add a photo before marking this done."
              }
            }
          }
        }
      },
      "CoverageDashboard": {
        "type": "object",
        "description": "The Coverage dashboard for one node and one lens.",
        "properties": {
          "scope": {
            "type": "object",
            "description": "Where the viewer is, for the breadcrumb and subtitle.",
            "properties": {
              "node": {
                "nullable": true,
                "description": "The current node ({id, name, level_name}); null at the top of the span."
              },
              "breadcrumb": {
                "type": "array",
                "description": "The span-clipped trail from the top of the span down to the current node.",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "name": {
                      "type": "string"
                    },
                    "level_name": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              },
              "child_level_name": {
                "type": "string",
                "nullable": true,
                "description": "The tenant's own word for the child tier (\"District\", \"Region\")."
              },
              "child_count": {
                "type": "integer"
              },
              "sites_reporting": {
                "type": "integer",
                "description": "Physical stores under the current node within the span."
              }
            }
          },
          "header": {
            "type": "object",
            "description": "The headline numbers, identical whichever lens is showing.",
            "properties": {
              "percent": {
                "type": "integer"
              },
              "tier": {
                "type": "string",
                "enum": [
                  "good",
                  "warn",
                  "bad"
                ],
                "description": "Completion band (≥90 good, ≥60 warn, else bad)."
              },
              "total": {
                "type": "integer"
              },
              "done": {
                "type": "integer"
              },
              "in_progress": {
                "type": "integer"
              },
              "not_started": {
                "type": "integer"
              },
              "missed": {
                "type": "integer"
              },
              "blocked": {
                "type": "integer",
                "description": "Obligations flagged blocked (an annotation on top of their status)."
              },
              "unassigned": {
                "type": "integer"
              },
              "sites_reporting": {
                "type": "integer"
              },
              "active_campaigns": {
                "type": "integer"
              },
              "scheduled_campaigns": {
                "type": "integer"
              }
            }
          },
          "view": {
            "type": "string",
            "enum": [
              "nodes",
              "campaign"
            ],
            "description": "The lens actually served."
          },
          "default_view": {
            "type": "string",
            "enum": [
              "nodes",
              "campaign"
            ]
          },
          "tabs": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "enum": [
                    "nodes",
                    "campaign"
                  ]
                },
                "label": {
                  "type": "string",
                  "description": "e.g. \"By district\", \"By campaign\"."
                },
                "active": {
                  "type": "boolean"
                }
              }
            }
          },
          "nodes": {
            "type": "array",
            "description": "The child-location cards (present only when view=nodes).",
            "items": {
              "$ref": "#/components/schemas/CoverageNodeCard"
            }
          },
          "campaigns": {
            "type": "array",
            "description": "The worst-first campaign cards (present only when view=campaign).",
            "items": {
              "$ref": "#/components/schemas/CoverageCampaignCard"
            }
          },
          "meta": {
            "type": "object",
            "description": "Pagination for the campaign lens (present only when view=campaign)."
          }
        }
      },
      "CoverageNodeCard": {
        "type": "object",
        "description": "A region / district / store, with its subtree rolled up. Semantic fields only — the client owns the colour.",
        "properties": {
          "location_id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "level_name": {
            "type": "string",
            "nullable": true
          },
          "leaf": {
            "type": "boolean",
            "description": "True when descending stops here (link to the store's day instead)."
          },
          "store_count": {
            "type": "integer"
          },
          "percent": {
            "type": "integer"
          },
          "tier": {
            "type": "string",
            "enum": [
              "good",
              "warn",
              "bad"
            ]
          },
          "total": {
            "type": "integer"
          },
          "done": {
            "type": "integer"
          },
          "in_progress": {
            "type": "integer"
          },
          "not_started": {
            "type": "integer"
          },
          "missed": {
            "type": "integer"
          },
          "blocked": {
            "type": "integer"
          },
          "unassigned": {
            "type": "integer"
          }
        }
      },
      "CoverageCampaignCard": {
        "type": "object",
        "description": "One campaign's slice of the caller's span. The client composes the meta line from these fields.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "priority": {
            "type": "string"
          },
          "work_type": {
            "type": "string"
          },
          "work_type_label": {
            "type": "string"
          },
          "recurring": {
            "type": "boolean"
          },
          "starts_on": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "ends_on": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "sites": {
            "type": "integer",
            "description": "Distinct locations the campaign touches in the span."
          },
          "percent": {
            "type": "integer"
          },
          "tier": {
            "type": "string",
            "enum": [
              "good",
              "warn",
              "bad"
            ]
          },
          "total": {
            "type": "integer"
          },
          "done": {
            "type": "integer"
          },
          "in_progress": {
            "type": "integer"
          },
          "awaiting_review": {
            "type": "integer",
            "description": "Obligations submitted and awaiting review — a SUBSET of in_progress, surfaced for the card's \"N awaiting review\" line."
          },
          "not_started": {
            "type": "integer"
          },
          "missed": {
            "type": "integer"
          },
          "blocked": {
            "type": "integer"
          },
          "unassigned": {
            "type": "integer"
          },
          "ready_to_close": {
            "type": "boolean",
            "description": "Active and every obligation settled (done or missed)."
          }
        }
      },
      "CoverageNudgeResult": {
        "type": "object",
        "description": "The outcome of a rollup nudge, counted in managers.",
        "properties": {
          "nudged": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The manager names who received a nudge."
          },
          "nudged_count": {
            "type": "integer"
          },
          "skipped": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Managers skipped — already nudged today, or no reachable manager."
          },
          "message": {
            "type": "string",
            "example": "Nudged 2 managers. 1 skipped — already nudged today, or no reachable manager."
          }
        },
        "required": [
          "nudged",
          "nudged_count",
          "skipped",
          "message"
        ]
      },
      "CoverageNudgeError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "nothing_to_nudge",
                  "nothing_behind",
                  "not_found"
                ]
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "CoverageRemindResult": {
        "type": "object",
        "description": "The outcome of a campaign-wide Remind, counted in people.",
        "properties": {
          "reminded": {
            "type": "integer",
            "description": "Distinct people who received at least one reminder."
          },
          "skipped": {
            "type": "integer",
            "description": "People who heard nothing at all — already reminded in the last 24 hours, or no reachable recipient."
          },
          "remaining": {
            "type": "integer",
            "description": "Outstanding people this pass didn't reach because the 200-delivery cap was hit — run Remind again to reach them."
          },
          "message": {
            "type": "string",
            "description": "A ready-to-show sentence, identical to the web Coverage notice.",
            "example": "Reminded 3 people. 1 didn't get one — already reminded in the last 24 hours, or no reachable recipient."
          }
        },
        "required": [
          "reminded",
          "skipped",
          "remaining",
          "message"
        ]
      },
      "DaySheetCounts": {
        "type": "object",
        "description": "Exact size of all eight filters (0 included). The six status buckets are a partition; `everything` is their union minus `completed`; `escalated` overlaps `blocked`.",
        "properties": {
          "everything": {
            "type": "integer",
            "example": 12
          },
          "escalated": {
            "type": "integer",
            "example": 1
          },
          "blocked": {
            "type": "integer",
            "example": 3
          },
          "unassigned": {
            "type": "integer",
            "example": 4
          },
          "sent_back": {
            "type": "integer",
            "example": 1
          },
          "in_review": {
            "type": "integer",
            "example": 2
          },
          "newly_assigned": {
            "type": "integer",
            "example": 2
          },
          "completed": {
            "type": "integer",
            "example": 9
          }
        }
      },
      "DaySheetListItem": {
        "type": "object",
        "description": "One row of the Day Sheet — a piece of work landing on a store today.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 90210
          },
          "campaign_id": {
            "type": "integer",
            "example": 512
          },
          "campaign": {
            "type": "string",
            "nullable": true,
            "example": "Pharmacy Cold Chain Audit"
          },
          "category": {
            "type": "object",
            "nullable": true,
            "description": "The campaign's programme (category), or null.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 42
              },
              "name": {
                "type": "string",
                "example": "Compliance"
              }
            }
          },
          "title": {
            "type": "string",
            "nullable": true,
            "example": "Fridge 2 — general stock"
          },
          "work_type": {
            "type": "string",
            "nullable": true,
            "description": "task / inspection / acknowledgement.",
            "example": "inspection"
          },
          "work_type_label": {
            "type": "string",
            "nullable": true,
            "example": "Inspection"
          },
          "location": {
            "type": "string",
            "nullable": true,
            "example": "Store 412 — Riverside"
          },
          "location_id": {
            "type": "integer",
            "example": 3369
          },
          "state": {
            "type": "string",
            "description": "The derived Day Sheet bucket this row belongs to.",
            "enum": [
              "blocked",
              "sent_back",
              "unassigned",
              "in_review",
              "newly_assigned",
              "completed",
              "other"
            ],
            "example": "blocked"
          },
          "status": {
            "type": "string",
            "description": "The raw lifecycle status behind the derived state.",
            "enum": [
              "open",
              "claimed",
              "in_progress",
              "submitted",
              "done",
              "reopened",
              "missed"
            ],
            "example": "in_progress"
          },
          "priority": {
            "type": "string",
            "enum": [
              "normal",
              "important",
              "critical"
            ],
            "example": "critical"
          },
          "overdue": {
            "type": "boolean",
            "example": true
          },
          "escalated": {
            "type": "boolean",
            "description": "This blocked row's blocker group has an open escalation.",
            "example": false
          },
          "due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "handoff_count": {
            "type": "integer",
            "description": "How many times this work has been handed off.",
            "example": 1
          },
          "requires_photo": {
            "type": "boolean",
            "nullable": true,
            "example": true
          },
          "requires_signature": {
            "type": "boolean",
            "nullable": true,
            "example": false
          },
          "requires_review": {
            "type": "boolean",
            "nullable": true,
            "example": true
          },
          "assignee": {
            "type": "object",
            "nullable": true,
            "description": "The current holder, or null for unassigned (pool) rows.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 771
              },
              "name": {
                "type": "string",
                "example": "Anthony Rivera"
              }
            }
          },
          "blocked_reason": {
            "type": "string",
            "nullable": true,
            "description": "Present when blocked — the machine reason code.",
            "example": "equipment_down"
          },
          "blocked_reason_label": {
            "type": "string",
            "nullable": true,
            "description": "Present when blocked — the human label for the reason.",
            "example": "Equipment is down"
          },
          "blocked_note": {
            "type": "string",
            "nullable": true,
            "description": "Present when blocked — the worker's free-text note.",
            "example": "Fridge 2 alarm is sounding, panel reads ERR."
          },
          "blocked_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "review_note": {
            "type": "string",
            "nullable": true,
            "description": "Present when sent_back — the reviewer's send-back note.",
            "example": "The left bay is still showing last season's card."
          },
          "reopened_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Present when in_review."
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Present when completed."
          }
        }
      },
      "DaySheetListMeta": {
        "type": "object",
        "description": "Pagination envelope for the SELECTED filter, plus the unrecognised-filter and unknown-category disclosures. Note the boolean keys are `has_next_page` / `has_prev_page`.",
        "properties": {
          "total_count": {
            "type": "integer",
            "example": 12
          },
          "total_pages": {
            "type": "integer",
            "example": 1
          },
          "current_page": {
            "type": "integer",
            "example": 1
          },
          "per_page": {
            "type": "integer",
            "example": 25
          },
          "has_next_page": {
            "type": "boolean",
            "example": false
          },
          "has_prev_page": {
            "type": "boolean",
            "example": false
          },
          "filter_ignored": {
            "type": "boolean",
            "description": "Present only when an unrecognised `filter` was defaulted to `everything`.",
            "example": true
          },
          "filter_note": {
            "type": "string",
            "description": "Human-readable note when `filter_ignored` is set."
          },
          "category_filter_ignored": {
            "type": "boolean",
            "description": "Present only when an unknown `category_id` was ignored.",
            "example": true
          },
          "category_filter_note": {
            "type": "string",
            "description": "Human-readable note when `category_filter_ignored` is set."
          }
        }
      },
      "DaySheetCategory": {
        "type": "object",
        "description": "One campaign category the sheet can be filtered to.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 42
          },
          "name": {
            "type": "string",
            "example": "Planogram"
          },
          "color_token": {
            "type": "string",
            "nullable": true,
            "description": "Bootstrap subtle-badge token for the category chip (primary/secondary/success/danger/warning/info/dark), or null.",
            "example": "info"
          },
          "active": {
            "type": "boolean",
            "description": "False for a retired category — still listed so a filtered historical sheet resolves.",
            "example": true
          }
        }
      },
      "DaySheetStore": {
        "type": "object",
        "description": "One store the sheet can be opened for (id + display name only).",
        "properties": {
          "id": {
            "type": "integer",
            "example": 3369
          },
          "name": {
            "type": "string",
            "example": "Store 412 — Riverside"
          }
        }
      },
      "DaySheetLocationsMeta": {
        "type": "object",
        "description": "Disclosure for the store list — count, cap state, and empty-state signal.",
        "properties": {
          "total": {
            "type": "integer",
            "description": "True store count in the caller's span (>= shown).",
            "example": 8
          },
          "shown": {
            "type": "integer",
            "description": "How many stores are returned (capped at 2,000).",
            "example": 8
          },
          "has_more": {
            "type": "boolean",
            "description": "True when the list was cut at the picker cap.",
            "example": false
          },
          "deactivated_only": {
            "type": "boolean",
            "description": "True when the span holds physical stores but every one is deactivated — lets a client distinguish \"switched off\" from \"none imported\".",
            "example": false
          }
        }
      },
      "ShiftBriefWindow": {
        "type": "object",
        "description": "The look-back window actually applied, plus the pickable options and snap disclosure.",
        "properties": {
          "since_hours": {
            "type": "integer",
            "description": "The window applied (one of the offered options).",
            "enum": [
              4,
              8,
              12,
              24,
              48,
              72
            ],
            "example": 12
          },
          "since": {
            "type": "string",
            "format": "date-time",
            "description": "The start of the window (now − since_hours)."
          },
          "options": {
            "type": "array",
            "description": "The pickable windows, for the selector.",
            "items": {
              "type": "integer"
            },
            "example": [
              4,
              8,
              12,
              24,
              48,
              72
            ]
          },
          "since_hours_ignored": {
            "type": "boolean",
            "description": "Present only when an unrecognised `since_hours` was defaulted to 12.",
            "example": true
          },
          "since_hours_note": {
            "type": "string",
            "description": "Human-readable note when `since_hours_ignored` is set."
          }
        }
      },
      "ShiftBriefBucketSummary": {
        "type": "object",
        "properties": {
          "count": {
            "type": "integer",
            "example": 3
          },
          "capped": {
            "type": "boolean",
            "description": "True when the list is at bucket_limit — there may be more.",
            "example": false
          },
          "total": {
            "type": "integer",
            "description": "The true total, present only for the counted buckets (completed / escalated).",
            "example": 43
          }
        }
      },
      "ShiftBriefRow": {
        "type": "object",
        "description": "One brief row, titled by campaign name (ShiftBrief does not resolve work titles).",
        "properties": {
          "id": {
            "type": "integer",
            "example": 1187
          },
          "campaign_id": {
            "type": "integer",
            "example": 42
          },
          "campaign": {
            "type": "string",
            "nullable": true,
            "example": "Pharmacy Cold Chain Audit"
          },
          "location": {
            "type": "string",
            "nullable": true,
            "example": "Fourth Street"
          },
          "location_id": {
            "type": "integer",
            "nullable": true,
            "example": 8
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "claimed",
              "in_progress",
              "submitted",
              "reopened",
              "done",
              "missed"
            ],
            "example": "claimed"
          },
          "overdue": {
            "type": "boolean",
            "example": true
          },
          "due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "assignee": {
            "type": "object",
            "nullable": true,
            "description": "The current holder — null for claim-pool rows.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 55
              },
              "name": {
                "type": "string",
                "example": "Anthony Rivera"
              }
            }
          }
        }
      },
      "ShiftBriefBlockedRow": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ShiftBriefRow"
          },
          {
            "type": "object",
            "description": "A blocked row also carries the structured blocker reason.",
            "properties": {
              "blocked_reason": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "no_stock",
                  "missing_fixture_or_supplies",
                  "equipment_down",
                  "not_enough_time",
                  "unclear_instructions",
                  "other"
                ]
              },
              "blocked_reason_label": {
                "type": "string",
                "nullable": true,
                "example": "Equipment down"
              },
              "blocked_note": {
                "type": "string",
                "nullable": true,
                "example": "Fridge panel unresponsive"
              }
            }
          }
        ]
      },
      "ShiftBriefCompletedRow": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ShiftBriefRow"
          },
          {
            "type": "object",
            "description": "A completed row also carries WHEN it was finished.",
            "properties": {
              "completed_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              }
            }
          }
        ]
      },
      "HeldCampaignsResult": {
        "type": "object",
        "properties": {
          "campaigns": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HeldCampaignCard"
            }
          },
          "total_count": {
            "type": "integer",
            "description": "How many held campaigns are in this response."
          },
          "capped": {
            "type": "boolean",
            "description": "True when the count hit the server cap (more may exist)."
          }
        },
        "required": [
          "campaigns",
          "total_count",
          "capped"
        ]
      },
      "HeldCampaignCard": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "description": "Always \"scheduled\" for a held campaign."
          },
          "priority": {
            "type": "string"
          },
          "work_type": {
            "type": "string"
          },
          "work_type_label": {
            "type": "string"
          },
          "would_launch_on": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "The date it is scheduled to launch if released."
          },
          "submitted_by": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time",
            "description": "When it entered the release gate."
          }
        },
        "required": [
          "id",
          "name",
          "status",
          "priority",
          "would_launch_on"
        ]
      },
      "FrontlineItemViewer": {
        "type": "object",
        "description": "The caller's lens on this obligation and the actions they may take.",
        "properties": {
          "is_holder": {
            "type": "boolean",
            "description": "The caller currently holds this obligation.",
            "example": false
          },
          "is_reviewer": {
            "type": "boolean",
            "description": "The caller may review work at this location.",
            "example": true
          },
          "can": {
            "type": "object",
            "description": "Which of the six item actions to show for this caller — mirrors the web display helpers. The write endpoints re-check at the transition.",
            "properties": {
              "claim": {
                "type": "boolean",
                "example": false
              },
              "complete": {
                "type": "boolean",
                "example": false
              },
              "release": {
                "type": "boolean",
                "example": false
              },
              "block": {
                "type": "boolean",
                "example": false
              },
              "challenge": {
                "type": "boolean",
                "example": false
              },
              "review": {
                "type": "boolean",
                "example": true
              }
            }
          }
        }
      },
      "FrontlineItemDetail": {
        "type": "object",
        "description": "One obligation in full. Lens-gated sections may be absent.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 18
          },
          "campaign_id": {
            "type": "integer",
            "example": 42
          },
          "campaign": {
            "type": "string",
            "nullable": true,
            "example": "Pharmacy Cold Chain Audit"
          },
          "category": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer",
                "example": 7
              },
              "name": {
                "type": "string",
                "example": "Compliance"
              }
            }
          },
          "title": {
            "type": "string",
            "example": "Pharmacy Cold Chain Audit"
          },
          "work_type": {
            "type": "string",
            "nullable": true,
            "example": "task"
          },
          "work_type_label": {
            "type": "string",
            "nullable": true,
            "example": "Task"
          },
          "location": {
            "type": "string",
            "nullable": true,
            "example": "Fourth Street"
          },
          "location_id": {
            "type": "integer",
            "nullable": true,
            "example": 12
          },
          "status": {
            "type": "string",
            "example": "in_progress"
          },
          "priority": {
            "type": "string",
            "example": "critical"
          },
          "overdue": {
            "type": "boolean",
            "example": true
          },
          "due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "estimated_minutes": {
            "type": "integer",
            "nullable": true,
            "example": 15
          },
          "handoff_count": {
            "type": "integer",
            "example": 0
          },
          "requires_photo": {
            "type": "boolean",
            "nullable": true,
            "example": true
          },
          "requires_signature": {
            "type": "boolean",
            "nullable": true,
            "example": true
          },
          "requires_review": {
            "type": "boolean",
            "nullable": true,
            "example": true
          },
          "blocked": {
            "type": "boolean",
            "example": false
          },
          "blocked_reason": {
            "type": "string",
            "nullable": true
          },
          "blocked_reason_label": {
            "type": "string",
            "nullable": true
          },
          "blocked_note": {
            "type": "string",
            "nullable": true
          },
          "blocked_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "review_note": {
            "type": "string",
            "nullable": true,
            "description": "The reviewer's send-back note (present only when reopened)."
          },
          "reopened_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "instruction": {
            "type": "string",
            "nullable": true,
            "description": "What HQ asked for, in full (the work's description, else the campaign's)."
          },
          "documents": {
            "type": "array",
            "description": "The campaign's reference documents (expiring download URLs).",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 91
                },
                "filename": {
                  "type": "string",
                  "example": "cold-chain-sop-v4.pdf"
                },
                "byte_size": {
                  "type": "integer",
                  "example": 2202009
                },
                "content_type": {
                  "type": "string",
                  "example": "application/pdf"
                },
                "url": {
                  "type": "string",
                  "example": "https://.../rails/active_storage/..."
                }
              }
            }
          },
          "holder": {
            "type": "object",
            "nullable": true,
            "description": "The current holder, or null when unclaimed.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 55
              },
              "name": {
                "type": "string",
                "example": "Priya Shah"
              }
            }
          },
          "accountable": {
            "type": "object",
            "nullable": true,
            "description": "The accountable manager — present only when unclaimed.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 3
              },
              "name": {
                "type": "string",
                "example": "Dana Okafor"
              }
            }
          },
          "training": {
            "type": "object",
            "description": "The completion training gate and any manager override.",
            "properties": {
              "requires_training": {
                "type": "boolean",
                "example": false
              },
              "courses": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer",
                      "example": 4
                    },
                    "title": {
                      "type": "string",
                      "example": "Cold Chain Handling"
                    }
                  }
                }
              },
              "override": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "granted_by": {
                    "type": "string",
                    "nullable": true,
                    "example": "Dana Okafor"
                  },
                  "reason": {
                    "type": "string",
                    "nullable": true
                  },
                  "granted_at": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          },
          "challenge": {
            "type": "object",
            "nullable": true,
            "description": "The assignment challenge in any state (\"this shouldn't be mine\"), or null.",
            "properties": {
              "status": {
                "type": "string",
                "example": "open"
              },
              "category": {
                "type": "string",
                "example": "not_my_area"
              },
              "category_label": {
                "type": "string",
                "example": "Not my area"
              },
              "note": {
                "type": "string",
                "nullable": true
              },
              "raised_at": {
                "type": "string",
                "nullable": true
              },
              "outcome": {
                "type": "string",
                "nullable": true
              },
              "reason": {
                "type": "string",
                "nullable": true
              },
              "resolved_at": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "checklist": {
            "type": "object",
            "description": "The ordered steps the holder works through — present ONLY when the minted work is a checklist task carrying steps (absent for an inspection, a simple task, or an unclaimed obligation with no work yet). Lens-independent: both holder and reviewer see it. `total`/`completed`/`percentage` come from the task's own counters.",
            "properties": {
              "total": {
                "type": "integer",
                "example": 5
              },
              "completed": {
                "type": "integer",
                "example": 2
              },
              "percentage": {
                "type": "integer",
                "example": 40
              },
              "items": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer",
                      "example": 310
                    },
                    "title": {
                      "type": "string",
                      "example": "Read fridge B from the front panel"
                    },
                    "description": {
                      "type": "string",
                      "nullable": true
                    },
                    "sort_order": {
                      "type": "integer",
                      "example": 0
                    },
                    "completed": {
                      "type": "boolean",
                      "example": true
                    },
                    "completed_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "completed_by": {
                      "type": "string",
                      "nullable": true,
                      "description": "Who ticked this step (null while pending).",
                      "example": "Priya Shah"
                    },
                    "requires_photo": {
                      "type": "boolean",
                      "example": false
                    },
                    "requires_notes": {
                      "type": "boolean",
                      "example": false
                    }
                  }
                }
              }
            }
          },
          "assignment_history": {
            "type": "object",
            "description": "REVIEWER LENS ONLY. Who held it and why it moved — newest first, capped at 50, with the true total.",
            "properties": {
              "total": {
                "type": "integer",
                "example": 3
              },
              "shown": {
                "type": "integer",
                "example": 3
              },
              "has_more": {
                "type": "boolean",
                "example": false
              },
              "events": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "event": {
                      "type": "string",
                      "example": "reassign"
                    },
                    "sentence": {
                      "type": "string",
                      "description": "The rendered one-line history sentence.",
                      "example": "Reassigned to Priya Shah by Dana Okafor"
                    },
                    "note": {
                      "type": "string",
                      "nullable": true
                    },
                    "occurred_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "review_proof": {
            "description": "REVIEWER LENS ONLY, and only when `status` is `submitted` — the inline proof the review queue renders (photos, signature, note, AI verdict). Same shape as the review-queue row's `proof`.",
            "type": "object"
          },
          "capture": {
            "type": "object",
            "description": "HOLDER / ROSTER LENS ONLY — what's already on the record.",
            "properties": {
              "captured_photos": {
                "type": "integer",
                "example": 2
              },
              "signature": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "signed": {
                    "type": "boolean",
                    "example": true
                  },
                  "signed_by": {
                    "type": "string",
                    "nullable": true,
                    "example": "Priya Shah"
                  },
                  "signed_at": {
                    "type": "string",
                    "nullable": true
                  }
                }
              },
              "notes_supported": {
                "type": "boolean",
                "example": true
              },
              "notes": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "author": {
                      "type": "string",
                      "example": "Priya Shah"
                    },
                    "body": {
                      "type": "string",
                      "example": "Stock landed on the 6am truck."
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "assignment_explanation": {
            "type": "object",
            "nullable": true,
            "description": "HOLDER LENS ONLY — \"why this is mine\", from the basis recorded when the person was bound.",
            "properties": {
              "sentence": {
                "type": "string",
                "example": "You're the nearest active manager for Fourth Street."
              },
              "checks": {
                "type": "string",
                "nullable": true
              },
              "rule": {
                "type": "string",
                "nullable": true,
                "example": "location_manager"
              },
              "recorded": {
                "type": "boolean",
                "example": true
              }
            }
          }
        }
      },
      "ItemNudgeResult": {
        "type": "object",
        "description": "The outcome of a per-item nudge.",
        "properties": {
          "nudged": {
            "type": "boolean",
            "description": "True when a fresh reminder was delivered; false when suppressed.",
            "example": true
          },
          "throttled": {
            "type": "boolean",
            "description": "True when nothing new was sent — already reminded in the last 24 hours, or the recipient couldn't be reached. Always the inverse of `nudged`.",
            "example": false
          },
          "recipient": {
            "type": "object",
            "description": "The person the reminder addressed (the holder, else the accountable manager).",
            "properties": {
              "id": {
                "type": "integer",
                "example": 4821
              },
              "name": {
                "type": "string",
                "example": "Dana Ruiz"
              }
            }
          },
          "message": {
            "type": "string",
            "description": "A ready-to-show sentence describing the outcome.",
            "example": "Reminder sent to Dana Ruiz."
          },
          "item": {
            "type": "object",
            "description": "The obligation the nudge was about, in the same lean shape the item write endpoints return."
          }
        },
        "required": [
          "nudged",
          "throttled",
          "recipient",
          "message",
          "item"
        ]
      },
      "ItemNudgeError": {
        "type": "object",
        "description": "The standard error envelope for a refused nudge.",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "`no_recipient` when there is nobody to nudge.",
                "example": "no_recipient"
              },
              "message": {
                "type": "string",
                "example": "Nobody holds this work and no manager covers its location, so there's nobody to nudge."
              }
            }
          }
        }
      },
      "MyDayListItem": {
        "type": "object",
        "description": "One My Day obligation (Execution::CampaignItem), with the fields the My Day row shows. A lean, list-optimised shape — the fuller single-item shape (assignment explanation, challenge, block detail) is returned by the item action endpoints.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 1187
          },
          "campaign_id": {
            "type": "integer",
            "example": 42
          },
          "campaign": {
            "type": "string",
            "nullable": true,
            "description": "The campaign's name — one campaign lands the same work at many sites.",
            "example": "Pharmacy Cold Chain Audit"
          },
          "title": {
            "type": "string",
            "nullable": true,
            "description": "The work's own title, falling back to the campaign name.",
            "example": "Pharmacy Cold Chain Audit"
          },
          "location": {
            "type": "string",
            "nullable": true,
            "example": "Fourth Street"
          },
          "location_id": {
            "type": "integer",
            "nullable": true,
            "example": 8
          },
          "status": {
            "type": "string",
            "description": "Lifecycle status of the obligation.",
            "enum": [
              "open",
              "claimed",
              "in_progress",
              "submitted",
              "reopened",
              "done",
              "missed"
            ],
            "example": "claimed"
          },
          "priority": {
            "type": "string",
            "description": "The campaign's HQ priority (absent reads as normal).",
            "enum": [
              "normal",
              "important",
              "critical"
            ],
            "example": "critical"
          },
          "overdue": {
            "type": "boolean",
            "example": true
          },
          "due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "rank_reason": {
            "type": "string",
            "description": "The same per-row \"why it's ranked here\" sentence the web My Day shows (overdue → critical → due today → due later → important → none). The anti-overload cap variant (\"more than your N critical\") is not reproduced on this list — a capped-over critical still reads \"Marked critical by HQ.\"",
            "example": "Overdue since 3:14 PM."
          },
          "estimated_minutes": {
            "type": "integer",
            "nullable": true,
            "description": "The campaign's estimated duration.",
            "example": 15
          },
          "requires_photo": {
            "type": "boolean",
            "nullable": true,
            "example": true
          },
          "requires_signature": {
            "type": "boolean",
            "nullable": true,
            "example": true
          },
          "requires_review": {
            "type": "boolean",
            "nullable": true,
            "example": true
          },
          "handoff_count": {
            "type": "integer",
            "example": 0
          },
          "blocked": {
            "type": "boolean",
            "description": "Whether the worker has flagged this as \"I can't do this\".",
            "example": false
          },
          "blocked_reason": {
            "type": "string",
            "nullable": true,
            "enum": [
              "no_stock",
              "missing_fixture_or_supplies",
              "equipment_down",
              "not_enough_time",
              "unclear_instructions",
              "other"
            ]
          },
          "claimable": {
            "type": "boolean",
            "description": "True only for claim-pool rows (nobody holds them yet).",
            "example": false
          },
          "assignee": {
            "type": "object",
            "nullable": true,
            "description": "The current holder — null for claim-pool rows.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 55
              },
              "name": {
                "type": "string",
                "example": "Anthony Rivera"
              }
            }
          }
        }
      },
      "MyDayListMeta": {
        "type": "object",
        "description": "Pagination envelope for the SELECTED filter, plus the unrecognised-filter disclosure. Note the boolean keys are `has_next_page` / `has_prev_page`.",
        "properties": {
          "total_count": {
            "type": "integer",
            "example": 7
          },
          "total_pages": {
            "type": "integer",
            "example": 1
          },
          "current_page": {
            "type": "integer",
            "example": 1
          },
          "per_page": {
            "type": "integer",
            "example": 25
          },
          "has_next_page": {
            "type": "boolean",
            "example": false
          },
          "has_prev_page": {
            "type": "boolean",
            "example": false
          },
          "filter_ignored": {
            "type": "boolean",
            "description": "Present only when an unrecognised `filter` was defaulted to `assigned`.",
            "example": true
          },
          "filter_note": {
            "type": "string",
            "description": "Human-readable note when `filter_ignored` is set."
          }
        }
      },
      "UnifiedDayRow": {
        "type": "object",
        "description": "One item of the worker's day, projected from its owning app. Acted on in that app — this surface only ranks and links.",
        "properties": {
          "source": {
            "type": "string",
            "description": "Which app this row came from.",
            "enum": [
              "frontline",
              "inspections",
              "must_reads",
              "tasks",
              "training",
              "shift_offers",
              "approvals"
            ],
            "example": "frontline"
          },
          "key": {
            "type": "string",
            "description": "Stable cross-source id, for de-dup across pages.",
            "example": "frontline-1187"
          },
          "title": {
            "type": "string",
            "example": "Close-out safety walk"
          },
          "subtitle": {
            "type": "string",
            "nullable": true,
            "description": "Location or lane context.",
            "example": "Store 412"
          },
          "priority": {
            "type": "string",
            "enum": [
              "critical",
              "important",
              "normal"
            ],
            "example": "normal"
          },
          "overdue": {
            "type": "boolean",
            "example": false
          },
          "due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "url": {
            "type": "string",
            "nullable": true,
            "description": "Where to act on the row, in the app that owns it. Null when that app resolved no route (render no link rather than a broken one).",
            "example": "/apps/frontline-execution/my-day"
          },
          "action_label": {
            "type": "string",
            "description": "The verb for the row's action.",
            "example": "Open"
          },
          "rank_reason": {
            "type": "string",
            "nullable": true,
            "description": "Why the row is ranked where it is.",
            "example": "Due 5:00 PM."
          }
        }
      },
      "UnifiedDayListMeta": {
        "type": "object",
        "description": "Pagination envelope for the SELECTED filter, plus the unrecognised-filter disclosure. Note the boolean keys are `has_next_page` / `has_prev_page`.",
        "properties": {
          "total_count": {
            "type": "integer",
            "example": 12
          },
          "total_pages": {
            "type": "integer",
            "example": 1
          },
          "current_page": {
            "type": "integer",
            "example": 1
          },
          "per_page": {
            "type": "integer",
            "example": 25
          },
          "has_next_page": {
            "type": "boolean",
            "example": false
          },
          "has_prev_page": {
            "type": "boolean",
            "example": false
          },
          "filter_ignored": {
            "type": "boolean",
            "description": "Present only when an unrecognised `filter` was defaulted to `all`.",
            "example": true
          },
          "filter_note": {
            "type": "string",
            "description": "Human-readable note when `filter_ignored` is set."
          }
        }
      },
      "FrontlineNoteItem": {
        "type": "object",
        "description": "The obligation after the note — the same shape the item-action endpoints return.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 18
          },
          "campaign_id": {
            "type": "integer",
            "example": 42
          },
          "title": {
            "type": "string",
            "example": "Reset the seasonal endcap"
          },
          "location": {
            "type": "string",
            "nullable": true,
            "example": "Fourth Street"
          },
          "location_id": {
            "type": "integer",
            "nullable": true,
            "example": 12
          },
          "status": {
            "type": "string",
            "example": "claimed"
          },
          "priority": {
            "type": "string",
            "nullable": true,
            "example": "high"
          },
          "due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "overdue": {
            "type": "boolean",
            "example": false
          },
          "handoff_count": {
            "type": "integer",
            "example": 0
          },
          "requires_photo": {
            "type": "boolean",
            "nullable": true,
            "example": true
          },
          "requires_signature": {
            "type": "boolean",
            "nullable": true,
            "example": false
          },
          "blocked": {
            "type": "boolean",
            "example": false
          }
        }
      },
      "FrontlineRecordedNote": {
        "type": "object",
        "description": "The note that was just recorded, ready to append to the thread.",
        "properties": {
          "author": {
            "type": "string",
            "description": "The note's author, as it appears in the trail (full name, falling back to name).",
            "example": "Dana Holder"
          },
          "body": {
            "type": "string",
            "description": "The note text (bounded to 500 characters).",
            "example": "Endcap reset done, but two SKUs were out of stock — flagged to the DM."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the trail activity was recorded (null only if activity logging degraded)."
          }
        }
      },
      "FrontlineNoteError": {
        "type": "object",
        "description": "The standard error envelope for a refused note.",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "`invalid_note`, `acknowledgement`, `unsupported_work` or `no_work`.",
                "example": "invalid_note"
              },
              "message": {
                "type": "string",
                "example": "Write the note first."
              }
            }
          }
        }
      },
      "ShiftPassdown": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 42
          },
          "status": {
            "type": "string",
            "enum": [
              "complete",
              "machine_closed"
            ],
            "description": "`complete` — a human filled it in.\n`machine_closed` — the shift ended without anyone being asked.\n"
          },
          "machine_closed": {
            "type": "boolean",
            "example": false
          },
          "location_id": {
            "type": "integer",
            "example": 1118
          },
          "author": {
            "nullable": true,
            "type": "object",
            "description": "Null on a machine_closed record — that is the point of it.",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "shift_ended_at": {
            "type": "string",
            "format": "date-time"
          },
          "notes": {
            "type": "string",
            "nullable": true
          },
          "structured": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "example": {
              "headcount": "14",
              "open_items": "2"
            }
          },
          "captured_via": {
            "type": "string",
            "enum": [
              "day_sheet",
              "wrap_up",
              "kiosk",
              "mobile",
              "api"
            ],
            "description": "Which surface collected it."
          },
          "acknowledged": {
            "type": "boolean"
          },
          "acknowledged_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "acknowledged_by": {
            "nullable": true,
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          }
        }
      },
      "FrontlineQuestion": {
        "type": "object",
        "description": "One Q&A comment — a question or a reply.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 512
          },
          "body": {
            "type": "string",
            "description": "Plain-text body, with @mentions rendered as @Name.",
            "example": "Do the small-format stores skip the endcap?"
          },
          "author": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer",
                "example": 88
              },
              "name": {
                "type": "string",
                "example": "Dana Ruiz"
              }
            }
          },
          "parent_id": {
            "type": "integer",
            "nullable": true,
            "description": "The question this replies to; null for a top-level question.",
            "example": null
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "replies": {
            "type": "array",
            "description": "Present on a top-level question in the GET thread; the replies to it, oldest first.",
            "items": {
              "$ref": "#/components/schemas/FrontlineQuestion"
            }
          }
        }
      },
      "FrontlineQuestionThread": {
        "type": "object",
        "properties": {
          "campaign": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "example": 42
              },
              "name": {
                "type": "string",
                "example": "Seasonal Endcap Reset"
              },
              "status": {
                "type": "string",
                "example": "active"
              },
              "accepting_questions": {
                "type": "boolean",
                "description": "Whether the campaign still takes NEW questions (false once closed).",
                "example": true
              }
            }
          },
          "questions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FrontlineQuestion"
            }
          },
          "question_count": {
            "type": "integer",
            "description": "The true total of top-level questions.",
            "example": 3
          },
          "meta": {
            "type": "object",
            "properties": {
              "showing": {
                "type": "integer",
                "example": 3
              },
              "total": {
                "type": "integer",
                "example": 3
              },
              "truncated": {
                "type": "boolean",
                "description": "True when there are more questions than the returned slice.",
                "example": false
              }
            }
          }
        }
      },
      "FrontlineQuestionError": {
        "type": "object",
        "description": "The standard error envelope for a refused question.",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "`forbidden`, `invalid_parent`, `blank` or `too_long`.",
                "example": "blank"
              },
              "message": {
                "type": "string",
                "example": "Write your question first."
              }
            }
          }
        }
      },
      "ItemReassignResult": {
        "type": "object",
        "description": "The outcome of a reassign — the reloaded obligation, now with the new holder.",
        "properties": {
          "item": {
            "type": "object",
            "description": "The obligation, in the same lean shape the other item write endpoints return (id, campaign_id, title, location, status, assignee, etc.)."
          }
        },
        "required": [
          "item"
        ]
      },
      "ItemReassignError": {
        "type": "object",
        "description": "The standard error envelope for a refused reassign.",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "One of `invalid_request`, `not_at_location`, `reason_required`, `already_assigned`, `already_done`, `in_review`, or a leave refusal.",
                "example": "not_at_location"
              },
              "message": {
                "type": "string",
                "example": "Dana Ruiz isn't assigned to Fourth Street."
              }
            }
          }
        }
      },
      "ReassignCandidatesResult": {
        "type": "object",
        "description": "The ranked store roster plus the item context and pagination meta.",
        "properties": {
          "item": {
            "type": "object",
            "description": "The obligation the roster is for.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 90123
              },
              "location_id": {
                "type": "integer",
                "example": 55
              },
              "location": {
                "type": "string",
                "example": "Fourth Street"
              },
              "current_assignee_id": {
                "type": "integer",
                "nullable": true,
                "description": "The present holder (null for an unclaimed pool item).",
                "example": 4821
              }
            }
          },
          "candidates": {
            "type": "array",
            "description": "The people the work can be handed to, ranked most-available first.",
            "items": {
              "$ref": "#/components/schemas/ReassignCandidate"
            }
          },
          "meta": {
            "type": "object",
            "description": "Standard pagination envelope. When the base roster hit the 100-person cap, also carries `roster_capped: true` and a `roster_note`.",
            "properties": {
              "total_count": {
                "type": "integer",
                "example": 12
              },
              "total_pages": {
                "type": "integer",
                "example": 1
              },
              "current_page": {
                "type": "integer",
                "example": 1
              },
              "per_page": {
                "type": "integer",
                "example": 25
              },
              "has_next_page": {
                "type": "boolean",
                "example": false
              },
              "has_prev_page": {
                "type": "boolean",
                "example": false
              },
              "roster_capped": {
                "type": "boolean",
                "description": "Present and true only when the store has more people than the cap.",
                "example": true
              },
              "roster_note": {
                "type": "string",
                "description": "Guidance to narrow the list with `q` when the roster was capped."
              }
            }
          }
        },
        "required": [
          "item",
          "candidates",
          "meta"
        ]
      },
      "ReassignCandidate": {
        "type": "object",
        "description": "One person on the reassign roster, with the availability signals the picker sorts by.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 4830
          },
          "name": {
            "type": "string",
            "example": "Priya Sharma"
          },
          "on_shift": {
            "type": "boolean",
            "description": "Whether the person is rostered at this store today.",
            "example": true
          },
          "open_load": {
            "type": "integer",
            "description": "The person's open workload (tenant-wide, due by end of day).",
            "example": 3
          },
          "on_leave": {
            "type": "boolean",
            "description": "Whether the person is on approved leave today. The reassign refuses them, so the picker labels it before the click.",
            "example": false
          },
          "is_current_assignee": {
            "type": "boolean",
            "description": "Whether this is the obligation's present holder.",
            "example": false
          }
        },
        "required": [
          "id",
          "name",
          "on_shift",
          "open_load",
          "on_leave",
          "is_current_assignee"
        ]
      },
      "ReleasedItem": {
        "type": "object",
        "description": "The item after release — the same shape the item-action endpoints return.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 90123
          },
          "campaign_id": {
            "type": "integer",
            "example": 42
          },
          "title": {
            "type": "string",
            "example": "Reset the seasonal endcap"
          },
          "location": {
            "type": "string",
            "nullable": true,
            "example": "Store 412 — Riverside"
          },
          "location_id": {
            "type": "integer",
            "nullable": true,
            "example": 3369
          },
          "status": {
            "type": "string",
            "description": "`open` after a successful release.",
            "example": "open"
          },
          "priority": {
            "type": "string",
            "nullable": true,
            "example": "high"
          },
          "due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Unchanged by release — the obligation still falls due when it did."
          },
          "overdue": {
            "type": "boolean",
            "example": false
          },
          "handoff_count": {
            "type": "integer",
            "example": 0
          },
          "claimable": {
            "type": "boolean",
            "example": false
          },
          "requires_photo": {
            "type": "boolean",
            "example": true
          },
          "requires_signature": {
            "type": "boolean",
            "example": false
          },
          "blocked": {
            "type": "boolean",
            "example": false
          },
          "assignment_explanation": {
            "type": "object",
            "nullable": true,
            "description": "Why the (former) holder had this work; null once it is back in the pool."
          }
        }
      },
      "ReleaseError": {
        "type": "object",
        "description": "The standard error envelope for a refused release.",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "`already_done`, `in_review` or `not_assigned`.",
                "example": "in_review"
              },
              "message": {
                "type": "string",
                "example": "This work is waiting for review and can't be released."
              }
            }
          }
        }
      },
      "ReleaseReview": {
        "type": "object",
        "properties": {
          "campaign": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "status": {
                "type": "string"
              },
              "priority": {
                "type": "string"
              },
              "work_type": {
                "type": "string"
              },
              "work_type_label": {
                "type": "string"
              }
            }
          },
          "held": {
            "type": "boolean",
            "description": "True when scheduled AND under publication review."
          },
          "would_launch_on": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "sites": {
            "type": "integer",
            "description": "Distinct locations the campaign would target."
          },
          "obligations": {
            "type": "integer",
            "description": "Projected obligations — one per resolved location."
          },
          "assignment_strategy": {
            "type": "string",
            "enum": [
              "location_manager",
              "single_assignee",
              "distributed",
              "claim_pool"
            ]
          },
          "assignment_strategy_label": {
            "type": "string"
          },
          "proof": {
            "type": "object",
            "properties": {
              "photo": {
                "type": "boolean"
              },
              "signature": {
                "type": "boolean"
              },
              "review": {
                "type": "boolean"
              }
            }
          },
          "window": {
            "type": "object",
            "properties": {
              "starts_on": {
                "type": "string",
                "format": "date",
                "nullable": true
              },
              "ends_on": {
                "type": "string",
                "format": "date",
                "nullable": true
              },
              "eligible_from": {
                "type": "string",
                "nullable": true,
                "description": "Wall-clock start, e.g. \"06:00\"."
              },
              "eligible_to": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "languages": {
            "type": "object",
            "properties": {
              "list": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Language codes the work would ship in (\"en\" plus each translation)."
              },
              "translations": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": "string"
                    },
                    "verified": {
                      "type": "boolean"
                    }
                  }
                }
              },
              "translations_verified": {
                "type": "boolean",
                "description": "True when every translation is verified and current."
              }
            }
          },
          "same_day": {
            "type": "object",
            "properties": {
              "campaigns": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "name": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    },
                    "obligations": {
                      "type": "integer"
                    }
                  }
                }
              },
              "total_obligations": {
                "type": "integer",
                "description": "This campaign's projection plus every same-day sibling's load."
              },
              "capped": {
                "type": "boolean",
                "description": "True when more same-day siblings exist than the response lists."
              }
            }
          },
          "approval": {
            "type": "object",
            "properties": {
              "under_review": {
                "type": "boolean"
              },
              "status": {
                "type": "string",
                "nullable": true,
                "description": "The open request's status (pending/escalated)."
              },
              "submitted_by": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  }
                }
              },
              "submitted_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "can_decide": {
                "type": "boolean",
                "description": "Whether THIS caller may release/hold it (engine can_approve?)."
              }
            }
          }
        },
        "required": [
          "campaign",
          "held",
          "sites",
          "obligations",
          "proof",
          "window",
          "languages",
          "same_day",
          "approval"
        ]
      },
      "PublicationDecisionResult": {
        "type": "object",
        "properties": {
          "campaign": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "description": "The post-decision status (a hold returns a scheduled campaign to \"draft\")."
              }
            }
          },
          "approval": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string",
                "description": "The request's terminal status (approved/rejected)."
              }
            }
          },
          "released": {
            "type": "boolean",
            "description": "True for a release",
            "false for a hold.": null
          },
          "message": {
            "type": "string",
            "description": "A ready-to-show sentence."
          }
        },
        "required": [
          "campaign",
          "approval",
          "released",
          "message"
        ]
      },
      "RequestDecisionError": {
        "type": "object",
        "description": "The standard error envelope for a refused intake-request decision.",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "`reason_required`, `already_decided`, `engine_owned` or `invalid`.",
                "example": "reason_required"
              },
              "message": {
                "type": "string",
                "example": "Say why — it's the only thing the person who asked for this will see."
              }
            }
          }
        }
      },
      "FrontlineRequestViewer": {
        "type": "object",
        "description": "The caller's lens on this intake request and the actions they may take.",
        "properties": {
          "is_requester": {
            "type": "boolean",
            "description": "The caller submitted this request.",
            "example": false
          },
          "is_gatekeeper": {
            "type": "boolean",
            "description": "The caller is in the campaign-author tier that works the queue.",
            "example": true
          },
          "can_approve": {
            "type": "boolean",
            "description": "Show the Approve control — the caller is a gatekeeper, the request is still pending, and no configured approval workflow owns the decision.",
            "example": true
          },
          "can_decline": {
            "type": "boolean",
            "description": "Show the Decline control — same condition as can_approve.",
            "example": true
          },
          "can_withdraw": {
            "type": "boolean",
            "description": "Show the Withdraw control — the caller is the submitter and the request is still pending.",
            "example": false
          }
        }
      },
      "FrontlineRequestDetail": {
        "type": "object",
        "description": "One intake request in full. Decision/campaign fields fill in as it moves through the funnel.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 31
          },
          "title": {
            "type": "string",
            "example": "Re-check cold chain after firmware fix"
          },
          "status": {
            "type": "string",
            "description": "submitted / approved / declined / withdrawn.",
            "example": "submitted"
          },
          "priority": {
            "type": "string",
            "example": "important"
          },
          "work_type": {
            "type": "string",
            "nullable": true,
            "example": "task"
          },
          "work_type_label": {
            "type": "string",
            "nullable": true,
            "example": "Task"
          },
          "instructions": {
            "type": "string",
            "nullable": true,
            "example": "Read both pharmacy fridges again and photograph the panel."
          },
          "note": {
            "type": "string",
            "nullable": true
          },
          "estimated_duration_minutes": {
            "type": "integer",
            "nullable": true,
            "example": 15
          },
          "desired_start_on": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "desired_end_on": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "desired_publish_on": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "category": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer",
                "example": 7
              },
              "name": {
                "type": "string",
                "example": "Compliance"
              }
            }
          },
          "department": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer",
                "example": 4
              },
              "name": {
                "type": "string",
                "example": "Pharmacy"
              }
            }
          },
          "audience": {
            "type": "object",
            "nullable": true,
            "description": "The requested audience, or null (\"Reviewer's call\" on the web).",
            "properties": {
              "id": {
                "type": "integer",
                "example": 12
              },
              "name": {
                "type": "string",
                "example": "5 pharmacy stores"
              }
            }
          },
          "target_location_count": {
            "type": "integer",
            "description": "How many stores the requester named directly (0 when they named an audience or left it to ops).",
            "example": 0
          },
          "requested_by": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer",
                "example": 55
              },
              "name": {
                "type": "string",
                "example": "Anthony Rivera"
              }
            }
          },
          "requested_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "decided_by": {
            "type": "object",
            "nullable": true,
            "description": "Who decided it — present once approved or declined.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 3
              },
              "name": {
                "type": "string",
                "example": "Dana Okafor"
              }
            }
          },
          "decided_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "decline_reason": {
            "type": "string",
            "nullable": true,
            "description": "The reason that travels back to the requester — present only on a declined request."
          },
          "under_engine_review": {
            "type": "boolean",
            "description": "The request is sitting in a configured approval workflow (decided in the Approvals surface, not the queue's buttons).",
            "example": false
          },
          "campaign": {
            "type": "object",
            "nullable": true,
            "description": "The draft campaign the approval minted — present only once approved.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 88
              },
              "name": {
                "type": "string",
                "example": "Re-check cold chain after firmware fix"
              },
              "status": {
                "type": "string",
                "example": "draft"
              }
            }
          },
          "attachments": {
            "type": "array",
            "description": "The requester's files (expiring download URLs).",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 91
                },
                "filename": {
                  "type": "string",
                  "example": "planogram.pdf"
                },
                "byte_size": {
                  "type": "integer",
                  "example": 220100
                },
                "content_type": {
                  "type": "string",
                  "example": "application/pdf"
                },
                "url": {
                  "type": "string",
                  "example": "https://.../rails/active_storage/..."
                }
              }
            }
          }
        }
      },
      "FrontlineRequestCounts": {
        "type": "object",
        "description": "One badge per visible tab. `approval` is present only for a reviewer.",
        "properties": {
          "all": {
            "type": "integer",
            "example": 12
          },
          "submitted": {
            "type": "integer",
            "description": "\"In review\" — the caller's own pending asks.",
            "example": 3
          },
          "approved": {
            "type": "integer",
            "example": 7
          },
          "declined": {
            "type": "integer",
            "example": 2
          },
          "approval": {
            "type": "integer",
            "description": "Reviewer only — every pending ask in the business (the queue badge).",
            "example": 5
          }
        }
      },
      "FrontlineRequestListRow": {
        "description": "One request row — the full request (as in the detail endpoint) plus the caller's per-row viewer.",
        "allOf": [
          {
            "type": "object",
            "description": "One intake request in full. Decision/campaign fields fill in as it moves through the funnel.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 31
              },
              "title": {
                "type": "string",
                "example": "Re-check cold chain after firmware fix"
              },
              "status": {
                "type": "string",
                "description": "submitted / approved / declined / withdrawn.",
                "example": "submitted"
              },
              "priority": {
                "type": "string",
                "example": "important"
              },
              "work_type": {
                "type": "string",
                "nullable": true,
                "example": "task"
              },
              "work_type_label": {
                "type": "string",
                "nullable": true,
                "example": "Task"
              },
              "instructions": {
                "type": "string",
                "nullable": true,
                "example": "Read both pharmacy fridges again and photograph the panel."
              },
              "note": {
                "type": "string",
                "nullable": true
              },
              "estimated_duration_minutes": {
                "type": "integer",
                "nullable": true,
                "example": 15
              },
              "desired_start_on": {
                "type": "string",
                "format": "date",
                "nullable": true
              },
              "desired_end_on": {
                "type": "string",
                "format": "date",
                "nullable": true
              },
              "desired_publish_on": {
                "type": "string",
                "format": "date",
                "nullable": true
              },
              "category": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "integer",
                    "example": 7
                  },
                  "name": {
                    "type": "string",
                    "example": "Compliance"
                  }
                }
              },
              "department": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "integer",
                    "example": 4
                  },
                  "name": {
                    "type": "string",
                    "example": "Pharmacy"
                  }
                }
              },
              "audience": {
                "type": "object",
                "nullable": true,
                "description": "The requested audience, or null (\"Reviewer's call\" on the web).",
                "properties": {
                  "id": {
                    "type": "integer",
                    "example": 12
                  },
                  "name": {
                    "type": "string",
                    "example": "5 pharmacy stores"
                  }
                }
              },
              "target_location_count": {
                "type": "integer",
                "description": "How many stores the requester named directly (0 when they named an audience or left it to ops).",
                "example": 0
              },
              "requested_by": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "integer",
                    "example": 55
                  },
                  "name": {
                    "type": "string",
                    "example": "Anthony Rivera"
                  }
                }
              },
              "requested_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "decided_by": {
                "type": "object",
                "nullable": true,
                "description": "Who decided it — present once approved or declined.",
                "properties": {
                  "id": {
                    "type": "integer",
                    "example": 3
                  },
                  "name": {
                    "type": "string",
                    "example": "Dana Okafor"
                  }
                }
              },
              "decided_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "decline_reason": {
                "type": "string",
                "nullable": true,
                "description": "The reason that travels back to the requester — present only on a declined request."
              },
              "under_engine_review": {
                "type": "boolean",
                "description": "The request is sitting in a configured approval workflow (decided in the Approvals surface, not the queue's buttons).",
                "example": false
              },
              "campaign": {
                "type": "object",
                "nullable": true,
                "description": "The draft campaign the approval minted — present only once approved.",
                "properties": {
                  "id": {
                    "type": "integer",
                    "example": 88
                  },
                  "name": {
                    "type": "string",
                    "example": "Re-check cold chain after firmware fix"
                  },
                  "status": {
                    "type": "string",
                    "example": "draft"
                  }
                }
              },
              "attachments": {
                "type": "array",
                "description": "The requester's files (expiring download URLs).",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer",
                      "example": 91
                    },
                    "filename": {
                      "type": "string",
                      "example": "planogram.pdf"
                    },
                    "byte_size": {
                      "type": "integer",
                      "example": 220100
                    },
                    "content_type": {
                      "type": "string",
                      "example": "application/pdf"
                    },
                    "url": {
                      "type": "string",
                      "example": "https://.../rails/active_storage/..."
                    }
                  }
                }
              }
            }
          },
          {
            "type": "object",
            "properties": {
              "viewer": {
                "type": "object",
                "description": "The caller's lens on this intake request and the actions they may take.",
                "properties": {
                  "is_requester": {
                    "type": "boolean",
                    "description": "The caller submitted this request.",
                    "example": false
                  },
                  "is_gatekeeper": {
                    "type": "boolean",
                    "description": "The caller is in the campaign-author tier that works the queue.",
                    "example": true
                  },
                  "can_approve": {
                    "type": "boolean",
                    "description": "Show the Approve control — the caller is a gatekeeper, the request is still pending, and no configured approval workflow owns the decision.",
                    "example": true
                  },
                  "can_decline": {
                    "type": "boolean",
                    "description": "Show the Decline control — same condition as can_approve.",
                    "example": true
                  },
                  "can_withdraw": {
                    "type": "boolean",
                    "description": "Show the Withdraw control — the caller is the submitter and the request is still pending.",
                    "example": false
                  }
                }
              }
            }
          }
        ]
      },
      "FrontlineRequestListMeta": {
        "type": "object",
        "description": "The standard pagination envelope, plus the filter-fallback disclosure.",
        "properties": {
          "total_count": {
            "type": "integer",
            "example": 12
          },
          "total_pages": {
            "type": "integer",
            "example": 1
          },
          "current_page": {
            "type": "integer",
            "example": 1
          },
          "per_page": {
            "type": "integer",
            "example": 25
          },
          "has_next_page": {
            "type": "boolean",
            "example": false
          },
          "has_prev_page": {
            "type": "boolean",
            "example": false
          },
          "filter_ignored": {
            "type": "boolean",
            "description": "Present and true when the requested filter was not applied (unknown, or `approval` for a non-reviewer).",
            "example": true
          },
          "filter_note": {
            "type": "string",
            "description": "Why the requested filter was not applied — present with filter_ignored.",
            "example": "The `filter` value \"approval\" was not applied (the `approval` queue is only available to reviewers); showing all your requests instead."
          }
        }
      },
      "FrontlineRequestCreateBody": {
        "type": "object",
        "description": "The fields the \"New request\" form submits. Only `title` is required; `priority` defaults to `normal`.",
        "required": [
          "title"
        ],
        "properties": {
          "title": {
            "type": "string",
            "maxLength": 255,
            "example": "Re-check cold chain after firmware fix"
          },
          "instructions": {
            "type": "string",
            "maxLength": 10000,
            "nullable": true,
            "description": "What the floor should actually do — carried onto the campaign the approval mints.",
            "example": "Read both pharmacy fridges again and photograph the panel."
          },
          "priority": {
            "type": "string",
            "enum": [
              "normal",
              "important",
              "critical"
            ],
            "default": "normal",
            "description": "Defaults to `normal` when omitted (the web form pre-selects it)."
          },
          "work_type": {
            "type": "string",
            "nullable": true,
            "enum": [
              "task",
              "inspection",
              "acknowledgement"
            ],
            "description": "The kind of work asked for. Blank = \"the requester didn't say, ops decides\".",
            "example": "task"
          },
          "owning_department_id": {
            "type": "integer",
            "nullable": true,
            "description": "The requesting department (dropped to null if it isn't this tenant's)."
          },
          "audience_id": {
            "type": "integer",
            "nullable": true,
            "description": "The requested audience (dropped to null if it isn't this tenant's). Null = \"Reviewer's call\"."
          },
          "category_id": {
            "type": "integer",
            "nullable": true,
            "description": "The intake programme/category (dropped to null if it isn't this tenant's)."
          },
          "estimated_duration_minutes": {
            "type": "integer",
            "nullable": true,
            "minimum": 1,
            "maximum": 480,
            "description": "How long one store should need (1–480 minutes).",
            "example": 15
          },
          "desired_start_on": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "desired_end_on": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "Must be on or after desired_start_on."
          },
          "desired_publish_on": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "note": {
            "type": "string",
            "maxLength": 2000,
            "nullable": true,
            "description": "A free-text note to the reviewer."
          },
          "target_location_ids": {
            "type": "array",
            "description": "Stores the requester named directly. Intersected with the tenant's active physical sites — ids outside it are silently dropped (never fail the ask).",
            "items": {
              "type": "integer"
            }
          },
          "store_list_paste": {
            "type": "string",
            "nullable": true,
            "description": "Pasted store numbers (one per line, or comma/tab separated). Resolved by external_id then exact name; the matched/unmatched split comes back in `store_list`.",
            "example": "4102\n4118\n4210"
          }
        }
      },
      "FrontlineRequestStoreListOutcome": {
        "type": "object",
        "nullable": true,
        "description": "How a pasted store list resolved. Present only when `store_list_paste`/`store_list_paste_file` was sent.",
        "properties": {
          "matched": {
            "type": "integer",
            "description": "How many pasted numbers matched a store.",
            "example": 2
          },
          "unmatched": {
            "type": "array",
            "description": "The unrecognised tokens (capped for display).",
            "items": {
              "type": "string"
            },
            "example": [
              "9999"
            ]
          },
          "unmatched_total": {
            "type": "integer",
            "description": "The true count of unrecognised tokens.",
            "example": 1
          },
          "has_more": {
            "type": "boolean",
            "description": "The unmatched list was capped for display.",
            "example": false
          }
        }
      },
      "ReviewDecisionItem": {
        "type": "object",
        "description": "The item after the decision — the same shape the item-action endpoints return.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 90123
          },
          "campaign_id": {
            "type": "integer",
            "example": 42
          },
          "title": {
            "type": "string",
            "example": "Reset the seasonal endcap"
          },
          "location": {
            "type": "string",
            "nullable": true,
            "example": "Store 412 — Riverside"
          },
          "location_id": {
            "type": "integer",
            "nullable": true,
            "example": 3369
          },
          "status": {
            "type": "string",
            "description": "`done` after accept, `reopened` after reject.",
            "example": "done"
          },
          "priority": {
            "type": "string",
            "nullable": true,
            "example": "high"
          },
          "due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "overdue": {
            "type": "boolean",
            "example": false
          },
          "handoff_count": {
            "type": "integer",
            "example": 0
          },
          "requires_photo": {
            "type": "boolean",
            "example": true
          },
          "requires_signature": {
            "type": "boolean",
            "example": false
          }
        }
      },
      "ReviewDecisionError": {
        "type": "object",
        "description": "The standard error envelope for a refused decision.",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "`reason_required`, `not_submitted` or `self_review`.",
                "example": "reason_required"
              },
              "message": {
                "type": "string",
                "example": "Say what needs fixing — it's the only thing the person who did this work will see."
              }
            }
          }
        }
      },
      "ReviewItem": {
        "type": "object",
        "description": "One submitted item awaiting review, with its proof inline.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 90123
          },
          "campaign_id": {
            "type": "integer",
            "example": 42
          },
          "campaign": {
            "type": "string",
            "nullable": true,
            "example": "Endcap reset — March"
          },
          "title": {
            "type": "string",
            "example": "Reset the seasonal endcap"
          },
          "location": {
            "type": "string",
            "nullable": true,
            "example": "Store 412 — Riverside"
          },
          "location_id": {
            "type": "integer",
            "nullable": true,
            "example": 3369
          },
          "status": {
            "type": "string",
            "example": "submitted"
          },
          "priority": {
            "type": "string",
            "nullable": true,
            "example": "high"
          },
          "due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "overdue": {
            "type": "boolean",
            "example": false
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "submitted_by": {
            "type": "object",
            "nullable": true,
            "description": "The worker who submitted the proof.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 771
              },
              "name": {
                "type": "string",
                "example": "Dana Ruiz"
              }
            }
          },
          "self_review_blocked": {
            "type": "boolean",
            "description": "True when this reviewer submitted the work themselves and separation of duties bars them from deciding it — accept/reject would be refused, so a client should show \"someone else has to approve it\".",
            "example": false
          },
          "requires_photo": {
            "type": "boolean",
            "example": true
          },
          "requires_signature": {
            "type": "boolean",
            "example": false
          },
          "proof": {
            "$ref": "#/components/schemas/ReviewProof"
          }
        }
      },
      "ReviewProof": {
        "type": "object",
        "description": "The evidence the reviewer decides on.",
        "properties": {
          "requires_photo": {
            "type": "boolean",
            "example": true
          },
          "requires_signature": {
            "type": "boolean",
            "example": false
          },
          "completion_note": {
            "type": "string",
            "nullable": true,
            "example": "Left bay reshot as asked."
          },
          "photos": {
            "$ref": "#/components/schemas/ReviewProofPhotos"
          },
          "signature": {
            "type": "object",
            "nullable": true,
            "description": "The captured signature, or null when none was required/collected.",
            "properties": {
              "image_data": {
                "type": "string",
                "description": "A `data:image/...` URL of the drawn signature."
              },
              "signed_by": {
                "type": "string",
                "nullable": true,
                "example": "Dana Ruiz"
              }
            }
          },
          "ai_check": {
            "type": "object",
            "nullable": true,
            "description": "The cached AI vision verdict on the photos, or null when AI is off for the business or nothing has been checked.",
            "properties": {
              "verdict": {
                "type": "string",
                "example": "looks_good"
              },
              "looks_good": {
                "type": "boolean",
                "example": true
              },
              "summary": {
                "type": "string",
                "example": "Shelves match the planogram; facings are full."
              },
              "checked_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "stale": {
                "type": "boolean",
                "description": "True when the verdict predates the latest resubmission or was run against a different photo count — a client should re-run before trusting it.",
                "example": false
              }
            }
          }
        }
      },
      "ReviewProofPhotos": {
        "type": "object",
        "description": "The completion photos, most recent first, capped and disclosed.",
        "properties": {
          "attached": {
            "type": "boolean",
            "example": true
          },
          "total": {
            "type": "integer",
            "description": "True photo count (>= shown.size).",
            "example": 8
          },
          "has_more": {
            "type": "boolean",
            "description": "True when the strip was cut at the 6-photo cap.",
            "example": true
          },
          "shown": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 55010
                },
                "filename": {
                  "type": "string",
                  "example": "endcap-left.jpg"
                },
                "url": {
                  "type": "string",
                  "description": "Full-size image URL (inline)."
                },
                "thumb_url": {
                  "type": "string",
                  "description": "140px thumbnail URL."
                }
              }
            }
          }
        }
      },
      "ReviewPaginationMeta": {
        "type": "object",
        "description": "The standard pagination envelope, plus the filter disclosure.",
        "properties": {
          "total_count": {
            "type": "integer",
            "example": 37
          },
          "total_pages": {
            "type": "integer",
            "example": 2
          },
          "current_page": {
            "type": "integer",
            "example": 1
          },
          "per_page": {
            "type": "integer",
            "example": 25
          },
          "has_next_page": {
            "type": "boolean",
            "example": true
          },
          "has_prev_page": {
            "type": "boolean",
            "example": false
          },
          "campaign_filter_ignored": {
            "type": "boolean",
            "description": "Present and true only when an unknown `campaign_id` was dropped.",
            "example": true
          },
          "campaign_filter_note": {
            "type": "string",
            "description": "Human-readable reason the campaign filter was not applied."
          }
        }
      },
      "ReadyToCloseCampaign": {
        "type": "object",
        "description": "One active campaign whose work is fully resolved.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 42
          },
          "name": {
            "type": "string",
            "example": "Endcap reset — March"
          },
          "status": {
            "type": "string",
            "example": "active"
          },
          "priority": {
            "type": "string",
            "nullable": true,
            "example": "normal"
          },
          "category": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer",
                "example": 7
              },
              "name": {
                "type": "string",
                "example": "Merchandising"
              }
            }
          },
          "work_type": {
            "type": "string",
            "example": "task"
          },
          "work_type_label": {
            "type": "string",
            "example": "Task"
          },
          "close_behavior": {
            "type": "string",
            "description": "manual / on_end_date / on_all_complete.",
            "example": "manual"
          },
          "starts_on": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "ends_on": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "total": {
            "type": "integer",
            "description": "Total obligations in the campaign.",
            "example": 40
          },
          "done": {
            "type": "integer",
            "description": "How many were signed off (done).",
            "example": 37
          },
          "missed": {
            "type": "integer",
            "description": "How many were missed (window closed).",
            "example": 3
          }
        }
      },
      "LibraryItemBookmarkState": {
        "type": "object",
        "description": "The caller's bookmark state for one library item AFTER the call. Identical for both verbs, so a client can write the result straight onto the item card it is holding with no field mapping.",
        "required": [
          "library_item_id",
          "bookmarked"
        ],
        "properties": {
          "library_item_id": {
            "type": "integer",
            "description": "The library item id (echoes the path id).",
            "example": 4217
          },
          "bookmarked": {
            "type": "boolean",
            "description": "The bookmark state for THIS caller after the call — always `true` from `POST`, always `false` from `DELETE`. Per-user: it says nothing about whether anyone else has saved the item.",
            "example": true
          },
          "unread_notification_count": {
            "type": "integer",
            "description": "Piggybacked on every response in this API for native badge management. Unrelated to Libraries.",
            "example": 3
          },
          "_meta": {
            "$ref": "#/components/schemas/ResponseMeta"
          }
        }
      },
      "LibraryCard": {
        "type": "object",
        "description": "One row of the All Libraries index. Every field the mockup's card renders, with the same fallbacks the web card applies — so the two surfaces cannot drift on an icon, a colour or a type label.",
        "required": [
          "id",
          "title",
          "description",
          "image",
          "banner_fill",
          "icon",
          "color",
          "library_type",
          "library_type_label",
          "categories_count",
          "items_count",
          "updated_at",
          "enabled",
          "can_disable",
          "link"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 42
          },
          "title": {
            "type": "string",
            "description": "The library's name.",
            "example": "Company Policies"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "The card's blurb. `null` (never `\"\"`) when the library has none.",
            "example": "Every published company policy, grouped by the team that owns it."
          },
          "image": {
            "type": "string",
            "nullable": true,
            "description": "ABSOLUTE URL of the library's banner image, or `null` when it has none — in which case paint `banner_fill`. Absolute because a native client cannot resolve a host-relative path.",
            "example": "https://acme.workforce.mangoapps.com/rails/active_storage/blobs/redirect/eyJf.../banner.png"
          },
          "banner_fill": {
            "type": "string",
            "description": "The CSS gradient the web card paints behind a library with no banner image. Sent whole rather than derived client-side: the light stop is a hand-picked per-preset value, so `color` alone is not enough to reproduce it.",
            "example": "linear-gradient(110deg,#3f5372,#7c8fb0)"
          },
          "icon": {
            "type": "string",
            "description": "Font Awesome class for the mark on the banner. Falls back to `fas fa-book-open` — the same default the web card uses — so this is never blank.",
            "example": "fas fa-file-shield"
          },
          "color": {
            "type": "string",
            "description": "The library's hex colour, driving both the icon tint and the banner fill. Falls back to the app default (`#2f64b1`), so this is never blank and never a non-hex value.",
            "example": "#3f5372"
          },
          "library_type": {
            "type": "string",
            "enum": [
              "mixed_content",
              "images_and_videos"
            ],
            "description": "The library kind, for client logic.",
            "example": "mixed_content"
          },
          "library_type_label": {
            "type": "string",
            "description": "The display wording the banner prints. Provided because `titleize` gets it wrong — `images_and_videos` reads \"Images & Videos\", not \"Images And Videos\".",
            "example": "Mixed Content"
          },
          "categories_count": {
            "type": "integer",
            "description": "Number of categories in this library (the folder icon on the card). Computed by a scalar subquery, so it costs nothing per row.",
            "example": 6
          },
          "items_count": {
            "type": "integer",
            "description": "Number of items across all of this library's categories (the document icon on the card). Also a scalar subquery.",
            "example": 28
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "ISO8601. The same timestamp `sort=recent` orders on and the card renders as \"Updated N ago\".",
            "example": "2026-08-29T06:32:39Z"
          },
          "enabled": {
            "type": "boolean",
            "description": "`false` for a disabled library — draw the \"Disabled\" badge and grey the row. Only a Libraries admin ever receives a `false` here.",
            "example": true
          },
          "can_disable": {
            "type": "boolean",
            "description": "Whether this caller may Disable / Enable this library. App-wide, so identical on every row of a response and equal to the top-level `can_disable`; carried per-row so a client rendering one card in isolation needs no extra context.",
            "example": true
          },
          "link": {
            "type": "string",
            "nullable": true,
            "description": "ABSOLUTE web URL of the library — for the row menu's Copy link, and for opening the library in a webview.",
            "example": "https://acme.workforce.mangoapps.com/apps/libraries/spaces/42"
          }
        }
      },
      "LibraryListMeta": {
        "type": "object",
        "description": "The canonical six-key pagination envelope (Api::V1::BaseController#build_pagination_meta), identical in shape to LibraryBookmarkPagination and LibrarySearchItemPagination. This schema declared only the first four until 2026-09-05: the endpoint was swept onto the shared helper without the contract being swept with it, so a client generated from this spec had no `has_next_page` and stopped paging at page 1 — the exact defect the code change closed.",
        "required": [
          "total_count",
          "current_page",
          "total_pages",
          "per_page",
          "has_next_page",
          "has_prev_page"
        ],
        "properties": {
          "total_count": {
            "type": "integer",
            "description": "Rows matching the applied filter for this caller — equal to the `filter_counts` entry for `active_filter`.",
            "example": 8
          },
          "current_page": {
            "type": "integer",
            "example": 1
          },
          "total_pages": {
            "type": "integer",
            "example": 1
          },
          "per_page": {
            "type": "integer",
            "example": 20
          },
          "has_next_page": {
            "type": "boolean",
            "description": "`current_page < total_pages`. Page until this is false; do not infer the end from a short page.",
            "example": false
          },
          "has_prev_page": {
            "type": "boolean",
            "example": false
          }
        }
      },
      "LibraryBookmarkedItem": {
        "type": "object",
        "description": "One saved library item — everything a row, its 3-dot sheet and its details panel need, from one call.",
        "required": [
          "id",
          "title",
          "item_type",
          "item_type_label",
          "icon_type",
          "icon",
          "breadcrumb",
          "status",
          "open_mode",
          "bookmarked"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "The LIBRARY ITEM id — what the bookmark write endpoints take.",
            "example": 412
          },
          "title": {
            "type": "string",
            "example": "Remote Work Policy"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "item_type": {
            "type": "string",
            "description": "The stored item kind. Branch logic on this, not on `item_type_label`.",
            "enum": [
              "simple_link",
              "file",
              "form",
              "survey",
              "wiki",
              "post",
              "image",
              "video"
            ],
            "example": "file"
          },
          "item_type_label": {
            "type": "string",
            "description": "Display label, EXTENSION-specific where the app can tell (\"PDF\", \"Word\", \"Excel\") and type-level otherwise (\"Link\", \"Form\", \"Wiki\"). The same label the web row prints.\nNAMED `item_type_label`, NOT `type_label`, since 2026-09-05: the column is `item_type`, every other label in this repo is `<prefix>_type_label` (including `library_type_label` on the library card and `item_type_label` on `GET /libraries/{id}`), and the bare spelling meant one concept arrived under two names depending on which Libraries endpoint a client called.",
            "example": "PDF"
          },
          "icon_type": {
            "type": "string",
            "description": "WHAT KIND OF STRING `icon` IS — the discriminator, because the glyph cannot carry its own domain. `default` and `custom` mean `icon` is a Font Awesome CLASS; `emoji` means `icon` is a raw emoji GRAPHEME.\nRead this before you render. A client that maps `icon` to an icon font unconditionally draws tofu for every emoji row, and one that prints it as text shows \"fas fa-file-pdf\" for the rest. No tenant holds an emoji row today, but Load Sample Data seeds three, so this is one button press from live rather than theoretical.\nNullable, because the column is: `library_items.icon_type` has no NOT NULL and the model's enum takes `allow_nil`, so an explicitly cleared row is a legal state. Treat null as `default`.",
            "enum": [
              "default",
              "emoji",
              "custom",
              null
            ],
            "nullable": true,
            "example": "default"
          },
          "icon": {
            "type": "string",
            "description": "The glyph — a Font Awesome class or an emoji grapheme, per `icon_type` above. Extension-specific where known; overridden by the item's own icon when an admin set one.",
            "example": "fas fa-file-pdf"
          },
          "icon_color": {
            "type": "string",
            "description": "Hex glyph colour.",
            "example": "#c0392b"
          },
          "icon_background": {
            "type": "string",
            "description": "Fill for the plate behind the glyph. Hex, or an `rgba()` string when derived from an admin-chosen item colour.",
            "example": "#fbe7e5"
          },
          "format": {
            "type": "string",
            "nullable": true,
            "description": "Lowercase file extension without the dot, or null when the item has nothing to infer one from (a form, a wiki page, a bare link).",
            "example": "pdf"
          },
          "library": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer",
                "example": 7
              },
              "name": {
                "type": "string",
                "example": "Company Policies"
              },
              "icon": {
                "type": "string",
                "example": "fas fa-shield-halved"
              },
              "color": {
                "type": "string",
                "example": "#3478f6"
              },
              "enabled": {
                "type": "boolean",
                "description": "False only for a Libraries administrator looking at a disabled library's saved item — grey the row and badge it \"Disabled\".",
                "example": true
              }
            }
          },
          "category": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer",
                "example": 21
              },
              "name": {
                "type": "string",
                "example": "Leave & Time Off"
              },
              "position": {
                "type": "integer",
                "example": 0
              }
            }
          },
          "breadcrumb": {
            "type": "string",
            "nullable": true,
            "description": "`Library › Category`, pre-joined with the separator the design uses.",
            "example": "Company Policies › Leave & Time Off"
          },
          "status": {
            "type": "string",
            "description": "`available` — opens normally. `inactive` — the linked record was unpublished/deactivated after it was linked, OR this reader may not open it; show the details panel, not a link. `unavailable` — the linked record is gone; a library manager must relink or remove it.",
            "enum": [
              "available",
              "inactive",
              "unavailable"
            ],
            "example": "available"
          },
          "status_label": {
            "type": "string",
            "nullable": true,
            "description": "The badge text the web renders beside the row (\"Inactive\", \"Source unavailable\"), or null when the item is healthy.",
            "example": null
          },
          "url": {
            "type": "string",
            "nullable": true,
            "description": "Absolute destination — where a tap goes. NULL whenever `open_mode` is `unavailable`, including for an item this particular reader may not open even though a link exists for someone else.\nALSO NULL when `open_mode` is `form`: a form is opened BY ID, from `form_id`. Branch on `open_mode`, never on which of the two is set."
          },
          "form_id": {
            "type": "integer",
            "nullable": true,
            "description": "The id of the linked form — the locator that replaced a form item's web URL, for `open_mode: form` only. Open your own form screen with it. Null for every other type, and null for a form this reader may not open (same rule `url` follows: `status` / `status_label` say why).",
            "example": null
          },
          "copy_link_url": {
            "type": "string",
            "nullable": true,
            "description": "What \"Copy link\" puts on the clipboard: the absolute destination. The same value as `url` for every type EXCEPT a form, where it is the card's only URL — the clipboard's job is to produce something a person can paste, and a form id pastes into nothing. Null whenever `open_mode` is `unavailable`."
          },
          "link_url": {
            "type": "string",
            "nullable": true,
            "description": "The source URL an admin stored on the item, which the details panel prints on its own row. Distinct from `url`, which is where a tap goes."
          },
          "opens_in": {
            "type": "string",
            "nullable": true,
            "description": "The item's configured link target — what the web anchor's `target` reads. `GET /libraries/{id}` carries the same column as `open.target`.\nNULLABLE, and a typed client must model it as optional. `library_items.link_target` is `t.string default: \"new_tab\"` with no NOT NULL, the model's enum takes `allow_nil`, and the item form deliberately submits an empty value — which casts back to nil — for every item type whose \"Open link in\" panel is never shown, so an unanswered question stays unanswered rather than being answered \"New tab\" on the admin's behalf. No row holds null today; the first one would crash a client with a non-optional String here.",
            "enum": [
              "new_tab",
              "current_tab",
              null
            ],
            "example": "new_tab"
          },
          "open_mode": {
            "type": "string",
            "description": "How to open it, and which of `url` / `form_id` carries the locator. `form` — a form this app hosts; route to your own form screen using `form_id` (`url` is null). `external` — an off-platform http(s) URL; hand it to the system browser. `preview` — a file this app serves; open the file preview or the media viewer. `in_app` — a path inside the platform (a wiki page, a survey, a document record). `unavailable` — nothing to open; show the details panel and `status_label` says why.",
            "enum": [
              "form",
              "external",
              "preview",
              "in_app",
              "unavailable"
            ],
            "example": "preview"
          },
          "download_url": {
            "type": "string",
            "nullable": true,
            "description": "Absolute URL for the same blob served as an attachment, or null when the item has no file to save (a link, a form, a wiki page, or a document record that stores no blob of its own). Omit the Download row from the sheet when null rather than showing it inert."
          },
          "media": {
            "type": "object",
            "nullable": true,
            "description": "The item's own uploaded file. Null when it has none.",
            "properties": {
              "filename": {
                "type": "string",
                "example": "remote-work.pdf"
              },
              "content_type": {
                "type": "string",
                "example": "application/pdf"
              },
              "byte_size": {
                "type": "integer",
                "example": 1153434
              },
              "size_label": {
                "type": "string",
                "description": "Pre-formatted, matching what the details panel prints.",
                "example": "1.1 MB"
              },
              "width": {
                "type": "integer",
                "nullable": true,
                "description": "Pixel width, ALWAYS an integer, and null — never zero — for a blob Active Storage has not analyzed yet.\nInteger is a coercion, not a passthrough: the two analyzers disagree on type. The image analyzer writes vips' Integer while the VIDEO analyzer writes `Float(video_stream[\"width\"])` by design, and this key used to ship whichever the row happened to hold — 2,000 image blobs as Integer, 149 video blobs as Float, under one key. Since 2026-09-05 both are coerced, which is safe because a pixel count is a whole number in either analyzer.",
                "example": 2400
              },
              "height": {
                "type": "integer",
                "nullable": true,
                "description": "Pixel height. Integer on the same terms as `width`.",
                "example": 1600
              },
              "dimensions": {
                "type": "string",
                "nullable": true,
                "description": "`width × height`, or null when either is unknown. Integral on both sides — it read \"1920.0 × 1080.0\" for every video until the coercion above landed.",
                "example": "2400 × 1600"
              },
              "duration_seconds": {
                "type": "number",
                "nullable": true,
                "description": "Video length. Null until background analysis has run on a freshly uploaded file — that is a real \"not yet known\", not a zero.",
                "example": 92.4
              },
              "duration_label": {
                "type": "string",
                "nullable": true,
                "description": "`m:ss` or `h:mm:ss`.",
                "example": "1:32"
              },
              "url": {
                "type": "string",
                "description": "Absolute URL for the blob served INLINE (preview / viewer)."
              }
            }
          },
          "created_by": {
            "type": "object",
            "nullable": true,
            "description": "Who added the item — the details panel's \"Added by\" byline. NAMED `created_by`, NOT `added_by`, since 2026-09-05: those are the names the columns actually have, and the names every other payload in this namespace uses (`GET /libraries/{id}` for both the item and the library). `added_by` / `added_at` were the only two occurrences of that spelling in the whole `/api/v1` surface — 2 sites against 254 — so a client decoding \"who added this, and when\" needed two field names depending on the endpoint.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 88
              },
              "name": {
                "type": "string",
                "example": "Neha Kulkarni"
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the ITEM was added to the library. Not to be confused with `bookmark.created_at`, which is when THIS CALLER saved it — the key this list is ordered by."
          },
          "updated_by": {
            "type": "object",
            "nullable": true,
            "description": "Null until somebody edits the item.",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "can_manage": {
            "type": "boolean",
            "description": "May this caller edit / move / delete items in this library. Gates the 3-dot sheet's Edit, Move and Delete rows; it follows the library's management level, not the caller's role alone.",
            "example": false
          },
          "bookmarked": {
            "type": "boolean",
            "description": "Always true on this endpoint. Present so one card type serves every list.",
            "example": true
          },
          "bookmark": {
            "type": "object",
            "nullable": true,
            "description": "The caller's own bookmark row.",
            "properties": {
              "id": {
                "type": "integer",
                "description": "The BOOKMARK id (not the item id). Useful for local ordering; the bookmark write endpoints address the ITEM, not this.",
                "example": 9021
              },
              "note": {
                "type": "string",
                "nullable": true,
                "description": "The caller's own note, authored on the web /bookmarks page.",
                "example": "Read before the audit"
              },
              "created_at": {
                "type": "string",
                "format": "date-time",
                "description": "When it was saved — the sort key of this list."
              }
            }
          }
        }
      },
      "LibraryBookmarkGroup": {
        "type": "object",
        "description": "One library the caller has saved something in, for the grouped Saved screen's header row. Deliberately a 7-key header rather than a full LibraryCard: it carries `bookmarked_count`, which the card has nowhere to put, and a card here would ship a signed banner URL, `link`, `banner_fill` and `description` per group header on the endpoint whose other defect was over-shipping this very array.\nSent on PAGE 1 ONLY — see `groups_total` on the response and the file header.",
        "required": [
          "id",
          "title",
          "bookmarked_count"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 7
          },
          "title": {
            "type": "string",
            "description": "The library's name. NAMED `title`, NOT `name`, since 2026-09-05: every other library representation in this namespace calls it `title` (the shared LibraryCard behind `/libraries/list`, `/libraries/search` and `GET /libraries/{id}`), and this hand-built group row was the one place the same column came back under a different key — so a client with one decoded `Library` type could not read its own Saved screen. A non-optional `title` in Swift or Kotlin is a hard decode failure, which takes down the whole screen rather than one row.",
            "example": "Company Policies"
          },
          "icon": {
            "type": "string",
            "example": "fas fa-shield-halved"
          },
          "color": {
            "type": "string",
            "example": "#3478f6"
          },
          "library_type": {
            "type": "string",
            "enum": [
              "mixed_content",
              "images_and_videos"
            ],
            "example": "mixed_content"
          },
          "enabled": {
            "type": "boolean",
            "example": true
          },
          "bookmarked_count": {
            "type": "integer",
            "description": "The caller's saved items in this library across the WHOLE set, not just this page.",
            "example": 1
          }
        }
      },
      "LibraryBookmarkPagination": {
        "type": "object",
        "properties": {
          "total_count": {
            "type": "integer",
            "description": "Saved items the caller can actually see. Deleted items and libraries out of reach are excluded here too, so this is the number the \"N bookmarked items\" line should print.",
            "example": 2
          },
          "total_pages": {
            "type": "integer",
            "example": 1
          },
          "current_page": {
            "type": "integer",
            "example": 1
          },
          "per_page": {
            "type": "integer",
            "example": 20
          },
          "has_next_page": {
            "type": "boolean",
            "example": false
          },
          "has_prev_page": {
            "type": "boolean",
            "example": false
          }
        }
      },
      "LibraryDetail": {
        "type": "object",
        "description": "The whole library screen in one payload.",
        "required": [
          "library",
          "sort",
          "available_sorts",
          "categories",
          "meta"
        ],
        "properties": {
          "library": {
            "$ref": "#/components/schemas/LibraryDetailHeader"
          },
          "sort": {
            "type": "string",
            "enum": [
              "default",
              "az",
              "recent"
            ],
            "description": "The order that was APPLIED, always a canonical key — so a client that sent the `updated` alias, or an unrecognised value, can tell what it actually got.",
            "example": "default"
          },
          "available_sorts": {
            "type": "array",
            "description": "The orders this endpoint accepts, for the order dropdown.",
            "items": {
              "type": "string"
            },
            "example": [
              "default",
              "az",
              "recent"
            ]
          },
          "categories": {
            "type": "array",
            "description": "The library's categories, in the order the applied `sort` puts them (position order under `default`). Empty categories are INCLUDED, so the response never hides structure. Empty array for a library with no categories, and for a `category_id` that is not in this library.",
            "items": {
              "$ref": "#/components/schemas/LibraryDetailCategory"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/LibraryDetailMeta"
          },
          "unread_notification_count": {
            "$ref": "#/components/schemas/UnreadNotificationCount"
          },
          "_meta": {
            "$ref": "#/components/schemas/ResponseMeta"
          }
        }
      },
      "LibraryDetailHeader": {
        "type": "object",
        "description": "The library card `/libraries/list` returns, plus the settings and capability flags only the detail screen needs. Field names and fallbacks are shared with that endpoint, so the two surfaces cannot drift on a name, a cover, a mark or a type label.\n`visibility`, `management_level` and `created_by` are MANAGER-ONLY and are omitted entirely — not nulled — when `can_manage_library` is false. They are correspondingly absent from `required` below.",
        "required": [
          "id",
          "title",
          "description",
          "image",
          "banner_fill",
          "icon",
          "color",
          "library_type",
          "library_type_label",
          "categories_count",
          "items_count",
          "enabled",
          "can_disable",
          "can_manage_items",
          "can_manage_library",
          "can_reorder_categories"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 312
          },
          "title": {
            "type": "string",
            "example": "Company Policies"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Null rather than an empty string when unset.",
            "example": "Everything HR publishes"
          },
          "image": {
            "type": [
              "string",
              "null"
            ],
            "description": "Absolute URL of the library's banner image, or null when none is attached — in which case a client paints `banner_fill`.",
            "example": "https://acme.workforce.mangoapps.com/rails/active_storage/blobs/redirect/…/banner.png"
          },
          "banner_fill": {
            "type": "string",
            "description": "The exact CSS gradient the web hero paints behind a library with no banner image. Sent whole because its light stop is a hand-picked per-preset value — a client could NOT derive it from `color`.",
            "example": "linear-gradient(110deg,#3f5372,#7c8fb0)"
          },
          "icon": {
            "type": "string",
            "description": "Font Awesome class. Defaults to `fas fa-book-open`.",
            "example": "fas fa-file-shield"
          },
          "color": {
            "type": "string",
            "description": "Hex colour, defaulted through the same helper the web card uses.",
            "example": "#3f5372"
          },
          "library_type": {
            "type": "string",
            "enum": [
              "mixed_content",
              "images_and_videos"
            ]
          },
          "library_type_label": {
            "type": "string",
            "description": "The product wording, which `titleize` gets wrong — \"Images & Videos\", not \"Images And Videos\".",
            "example": "Mixed Content"
          },
          "visibility": {
            "type": "string",
            "enum": [
              "all_users",
              "specific_audiences"
            ],
            "description": "MANAGER-ONLY. The key is ABSENT — not null — unless `can_manage_library` below is true.\n`visibility` and `management_level` are the library's ACCESS RULES, and they shipped to every reader until 2026-09-04. Together they hand a plain member a map of which libraries in the tenant are restricted and, worse, exactly which carry `management_level: anyone` — i.e. where item writes are open to every reader. Neither is rendered to a non-manager on the web either; both live in the library form, behind the same predicate.\nA manager who receives them may not necessarily EDIT them: that is a second, stricter predicate (`SpacesController#library_audience_editable?` — admin, app-admin, or the library's creator), and it has no flag in this payload yet, so a client must not infer write access from the key's presence."
          },
          "management_level": {
            "type": "string",
            "enum": [
              "anyone",
              "admins_and_specific",
              "admins_only",
              "domain_admins_only"
            ],
            "description": "MANAGER-ONLY, on exactly the same terms as `visibility` above — absent unless `can_manage_library` is true, and read-only for some of the managers who receive it."
          },
          "default_view_mode": {
            "type": "string",
            "enum": [
              "grid",
              "large",
              "compact"
            ],
            "description": "The library's own default, which SEEDS a new category's layout and does not override an existing one. Every category below carries the `view` it was actually given; read that, not this, to render."
          },
          "show_category_icons": {
            "type": "boolean",
            "description": "When false, a client omits the category heading's icon entirely."
          },
          "show_item_icons": {
            "type": "boolean",
            "description": "When false, a client omits the per-item mark entirely."
          },
          "categories_count": {
            "type": "integer",
            "description": "The library's TRUE number of categories — the same value `/libraries/list` and `/libraries/search` return on the shared LibraryCard, and unaffected by `?category_id=`.\nIt used to be overridden here with a count of what the response happened to carry, which made a deep link report that the library held almost nothing: measured 2026-09-05 on a library truly holding 3 categories / 180 items, `/list` said 3 and 180, `GET /{id}` said 3 and 180, and `GET /{id}?category_id=…` said **1 and 60** — same library, same field, same session. A client that cached the card from the list and refreshed it from a deep link overwrote a correct total with a scoped one and had no field left to recover the real number from.\nThe response-scoped pair still exists, under names that say so: `meta.categories_count` / `meta.items_count`.",
            "example": 6
          },
          "items_count": {
            "type": "integer",
            "description": "The library's TRUE number of items across every category, on the same terms as `categories_count` above. Not the length of anything in this payload — that is `meta.items_count`.",
            "example": 28
          },
          "created_by": {
            "allOf": [
              {
                "$ref": "#/components/schemas/LibraryUserRef"
              }
            ],
            "description": "MANAGER-ONLY. The key is ABSENT — not null — unless `can_manage_library` below is true, on exactly the same terms as `visibility` and `management_level` above.\nWHO CREATED A LIBRARY IS AN AUTHORIZATION FACT here, not provenance: `libraries.created_by_id` is read by `Libraries::Access#can_manage_library?` and by the stricter `SpacesController#library_audience_editable?`, so this field names, per library, the one person outside the admin tier who can rewrite that library's audience. It moved into `DetailsController#manager_only_audience_payload` on 2026-09-05, alongside the two audience fields that moved there on 2026-09-04.\nThis schema still listed it as an unconditional field until 2026-09-06, so a spec-generated client bound a byline to a key a plain reader never receives. (The item-level `created_by` in `LibraryItem` further down is a different field — a plain byline, sent to every reader.)"
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "enabled": {
            "type": "boolean",
            "description": "False for a disabled library. Only a caller who can manage the library reaches this payload at all in that state (everyone else 404s), so `false` here means \"badge it Disabled and offer Enable\"."
          },
          "can_disable": {
            "type": "boolean",
            "description": "Whether this caller may flip the library's enabled state. App-wide rather than per-library, and the same authority `POST /libraries/{id}/disable` gates on."
          },
          "can_manage_items": {
            "type": "boolean",
            "description": "Gates Add Item and the item kebab's Edit / Move / Delete. True for a Libraries admin, for anyone who can view an `anyone`-managed library, and for a contributor or manager under `admins_and_specific`."
          },
          "can_manage_library": {
            "type": "boolean",
            "description": "Gates Add Category, Edit library and Reorder categories. True for a Libraries admin and for a named `manager`."
          },
          "can_reorder_categories": {
            "type": "boolean",
            "description": "Offered only when the response is actually SHOWING the order those stored positions control: `can_manage_library` AND `sort` is `default` AND there is more than one category. Under `az` or `recent` the admin would be arranging rows whose new order the screen in front of them never reflects.\nTHERE IS NO REORDER ENDPOINT IN THIS NAMESPACE. This flag answers \"may you\"; `link` below answers \"where\" — the Reorder categories dialog is a modal on the library page, so the page is its address. No separate `manage_url` is minted at this level because it would hold the identical string. See `LibraryItemActions`."
          },
          "link": {
            "type": [
              "string",
              "null"
            ],
            "description": "Absolute web URL for the library — for Copy link, for a webview hand-off, and the destination for `can_reorder_categories` / `can_manage_library` above, whose actions (Reorder categories, Add category, Edit library, Add item) all live on that page and have no endpoint here.",
            "example": "https://acme.workforce.mangoapps.com/apps/libraries/spaces/312"
          }
        }
      },
      "LibraryDetailCategory": {
        "type": "object",
        "description": "One category block. `view` and `sort_order` are the admin-owned columns that decide how this section renders and sorts for EVERY reader.",
        "required": [
          "id",
          "name",
          "icon",
          "position",
          "view",
          "view_label",
          "sort_order",
          "items_sorted_by",
          "items_count",
          "items_truncated",
          "can_reorder_items",
          "manage_url",
          "items"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 908
          },
          "name": {
            "type": "string",
            "example": "Site Procedures"
          },
          "icon": {
            "type": "string",
            "description": "Font Awesome class. Defaults to `fas fa-folder`.",
            "example": "fas fa-clipboard-check"
          },
          "position": {
            "type": "integer",
            "description": "The category's stored order within the library."
          },
          "view": {
            "type": "string",
            "enum": [
              "compact",
              "large",
              "grid"
            ],
            "description": "The layout this category renders in — `compact` (title-only rows), `large` (rows with description and meta line) or `grid` (thumbnail tiles). Set by an admin on the category and identical for every reader; there is no per-user view switcher."
          },
          "view_label": {
            "type": "string",
            "description": "The product wording for `view`, as the category form offers it.",
            "example": "Large"
          },
          "sort_order": {
            "type": "string",
            "enum": [
              "admin_defined",
              "a_to_z",
              "z_to_a"
            ],
            "description": "The category's OWN ordering. `admin_defined` is the hand-ordered sequence the Reorder dialog writes to each item's `position`. This is what `sort=default` honours per category."
          },
          "items_sorted_by": {
            "type": "string",
            "enum": [
              "admin_defined",
              "a_to_z",
              "z_to_a",
              "az",
              "recent"
            ],
            "description": "The rule that actually produced the `items` array below — this category's `sort_order` under `sort=default`, otherwise the page-wide override. Without it a client showing \"Sorted A→Z\" per section could not tell whether that was the reader's choice or the admin's."
          },
          "items_count": {
            "type": "integer",
            "description": "The category's TRUE item total — NOT the length of `items`, which is capped at 500 (`MAX_ITEMS_PER_CATEGORY`). The two are equal unless `items_truncated` is true. Zero for an empty category, which is still returned."
          },
          "items_truncated": {
            "type": "boolean",
            "description": "True when this category holds more than 500 items and `items` below therefore carries only the first 500 in the applied order — `items_count` keeps the real number.\nThe cap exists so one GET cannot be an unbounded read of a whole library; it is far above anything the product produces (measured across every tenant 2026-09-04: the largest category holds 60 items), so it truncates nothing that exists today. Read the flag anyway — the rule here is that truncation is REPORTED, never silent, because a client quietly missing half a category is a worse bug than the unbounded read the cap replaced.",
            "example": false
          },
          "can_reorder_items": {
            "type": "boolean",
            "description": "Offered only when the response is showing the hand-ordered sequence: `can_manage_items` AND `sort` is `default` AND this category's `sort_order` is `admin_defined` AND it holds more than one item. The same four-part gate the web kebab applies.\nTHERE IS NO REORDER ENDPOINT IN THIS NAMESPACE — this flag answers \"may you\", and `manage_url` below answers \"where\". See that field."
          },
          "manage_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Absolute web URL for this category's block on the library page — where its kebab (Edit category, Reorder items) lives. The dialog `can_reorder_items` refers to is a modal rendered into that page rather than a screen with an address of its own, so this is the library page anchored at `#category-{id}`.\nNull unless the caller can manage this library or its items — the same predicates the web page applies, so the URL is never a page its recipient would be bounced off. The KEY is always present; read the null, do not test for the key.\nHand it to a web view or the system browser. See `LibraryItemActions` for why the management flags in this response ship URLs instead of API verbs.",
            "example": "https://acme.workforce.mangoapps.com/apps/libraries/spaces/312#category-908"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LibraryDetailItem"
            }
          }
        }
      },
      "LibraryDetailItem": {
        "type": "object",
        "description": "One item, carrying everything the mockup's three surfaces read: the row or tile, the ⋯ actions sheet, and the \"View details\" drawer that sheet opens.",
        "required": [
          "id",
          "title",
          "description",
          "position",
          "item_type",
          "item_type_label",
          "display_label",
          "icon",
          "icon_color",
          "icon_background",
          "status",
          "bookmarked",
          "manage_url",
          "actions"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 4217
          },
          "title": {
            "type": "string",
            "example": "Working at Height — Safety Harness Procedure"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Null rather than an empty string when unset. Rendered by the `large` layout."
          },
          "position": {
            "type": "integer",
            "description": "The item's stored order within its category."
          },
          "item_type": {
            "type": "string",
            "enum": [
              "simple_link",
              "file",
              "form",
              "survey",
              "wiki",
              "post",
              "image",
              "video"
            ]
          },
          "item_type_label": {
            "type": "string",
            "description": "The human word for `item_type`.",
            "example": "File"
          },
          "display_label": {
            "type": "string",
            "description": "What the row's plate and the tile's panel actually PRINT: the FORMAT where it can be told (\"PDF\", \"Excel\", \"Word\"), the type otherwise (\"Link\", \"Form\"). On a phone row the format is the useful signal, not the generic word \"File\".",
            "example": "PDF"
          },
          "icon": {
            "type": "string",
            "description": "Font Awesome class, resolved by the same helper the web listing uses — format-specific where possible, and overridden by the item's own icon when an admin set one.",
            "example": "fas fa-file-pdf"
          },
          "icon_color": {
            "type": "string",
            "description": "Hex tint for the glyph. The item's own `icon_color` wins when set.",
            "example": "#c0392b"
          },
          "icon_background": {
            "type": "string",
            "description": "Fill for the plate behind the glyph. Hex from the type/format palette, or an `rgba()` wash of the item's own colour when one is set.",
            "example": "#fbe7e5"
          },
          "thumbnail_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Absolute URL of the picture to show instead of the plate. Present for an IMAGE item with a real raster attached; null for everything else — no page rendering or video poster is invented for a document or a video."
          },
          "library_id": {
            "type": "integer"
          },
          "library_name": {
            "type": "string"
          },
          "category_id": {
            "type": "integer"
          },
          "category_name": {
            "type": "string"
          },
          "breadcrumb": {
            "type": "string",
            "description": "\"Library › Category\" — the actions sheet's header line, pre-composed so a client does not re-join it.",
            "example": "Company Policies › Site Procedures"
          },
          "status": {
            "type": "string",
            "enum": [
              "available",
              "inactive",
              "unavailable"
            ],
            "description": "The state of the record this item points at. `inactive` — the linked form / survey / wiki page exists but is not in its openable state (unpublished, draft, deactivated). `unavailable` — the linked record is gone. `available` for a healthy item and for any item with nothing linked."
          },
          "status_label": {
            "type": [
              "string",
              "null"
            ],
            "description": "The badge text for a bad state (\"Inactive\", \"Source unavailable\"), null when `available`."
          },
          "status_message": {
            "type": [
              "string",
              "null"
            ],
            "description": "The details drawer's own explanatory sentence for a bad state, so a client does not re-word it. Null when `available`."
          },
          "created_by": {
            "$ref": "#/components/schemas/LibraryUserRef"
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "The drawer's \"Added by … · date\" row."
          },
          "updated_by": {
            "$ref": "#/components/schemas/LibraryUserRef"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "file": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/LibraryItemFile"
              },
              {
                "type": "null"
              }
            ],
            "description": "The uploaded blob's own description — the drawer's Format / Size / Dimensions / Duration rows. NULL for anything with no blob of its own: a link, a form, a wiki item, and a `file` item that points at an HR File Manager record rather than carrying an upload (reaching that record's metadata means walking a version chain once per row, which is an N+1 a list endpoint must not ship — such an item still carries `open`, which is where the document is served from)."
          },
          "link_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "The item's stored source URL — the drawer's \"URL\" row. For a simple link this is the same as `open.url`; for other types it is the raw field an admin typed. Null when unset."
          },
          "open": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/LibraryItemOpen"
              },
              {
                "type": "null"
              }
            ],
            "description": "Where and how the item opens, or NULL when there is nowhere to go — a dangling linked record, an unattached media item, a non-http link, or a record this caller may not reach. A client must treat null as \"render the title as plain text\", never as an error; that is exactly what the web listing does."
          },
          "copy_link_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "What the kebab's \"Copy link\" puts on the clipboard: the destination made ABSOLUTE, because a copied link is pasted elsewhere and a host-less path is broken the moment it lands. Null whenever `open` is.\nThis stays a URL for a FORM item, whose `open` block carries only a `form_id` — the clipboard's job is to produce something a person can paste, and an id pastes into nothing."
          },
          "download": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/LibraryItemDownload"
              },
              {
                "type": "null"
              }
            ],
            "description": "The item's own blob, served with an attachment disposition — the same blob `open` shows, saved rather than displayed. Null when the item carries no upload."
          },
          "bookmarked": {
            "type": "boolean",
            "description": "Whether THIS caller has saved the item. Per-user, and the same platform bookmark `POST`/`DELETE /libraries/items/{id}/bookmark` writes, so the flag and those verbs cannot disagree."
          },
          "manage_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Absolute web URL for this item's edit form — the destination for BOTH `actions.edit` and `actions.move`, which are one screen: the form carries the Category select, so moving an item is an edit of its category rather than a second page.\nNOT a second copy of `open` / `copy_link_url`. Those point at the item's DESTINATION — the wiki page, the form, the blob — while this points at the Libraries management UI. A client that acted on `actions.edit` previously had nothing in the response to navigate to.\nNull unless `actions.edit` is true (the same `can_manage_items` predicate the web controller applies), so it is never a page its recipient would be bounced off. The KEY is always present; read the null, do not test for the key.\nHand it to a web view or the system browser. See `LibraryItemActions` for why.",
            "example": "https://acme.workforce.mangoapps.com/apps/libraries/spaces/312/items/4471/edit"
          },
          "actions": {
            "$ref": "#/components/schemas/LibraryItemActions"
          }
        }
      },
      "LibraryItemFile": {
        "type": "object",
        "description": "The uploaded blob's format, size and media properties.",
        "properties": {
          "filename": {
            "type": "string",
            "example": "harness-procedure.pdf"
          },
          "extension": {
            "type": [
              "string",
              "null"
            ],
            "description": "Lowercase, no dot. Null for a format the style table has no entry for.",
            "example": "pdf"
          },
          "format_label": {
            "type": "string",
            "description": "The human word for the format, the same one the tile's plate prints.",
            "example": "PDF"
          },
          "content_type": {
            "type": "string",
            "example": "application/pdf"
          },
          "byte_size": {
            "type": "integer",
            "example": 1258291
          },
          "size_label": {
            "type": "string",
            "description": "Human-readable size, pre-formatted.",
            "example": "1.2 MB"
          },
          "width": {
            "type": [
              "integer",
              "null"
            ],
            "description": "From the Active Storage analyzer. NULL — not zero — for a blob that has not been analyzed yet, so a client renders nothing rather than \"0 × 0\".",
            "example": 2400
          },
          "height": {
            "type": [
              "integer",
              "null"
            ],
            "example": 1600
          },
          "dimensions": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pre-composed \"W × H\", null unless both are known.",
            "example": "2400 × 1600"
          },
          "duration_seconds": {
            "type": [
              "number",
              "null"
            ],
            "description": "Video / audio only, from the analyzer.",
            "example": 198.4
          },
          "duration_label": {
            "type": [
              "string",
              "null"
            ],
            "description": "`duration_seconds` as m:ss.",
            "example": "3:18"
          }
        }
      },
      "LibraryItemOpen": {
        "type": "object",
        "description": "The resolved destination and how a client should present it.\nTWO SHAPES, told apart by `mode`. A `form` item is opened BY ID: it carries `form_id` and NO `url` / `path`, because a client routes to its own form screen and a web path it would have to parse was useless to it. Everything else carries `url` + `path` and no `form_id`. Branch on `mode`, never on which key happens to be present.\nA form's pasteable web URL is still available — as the item's `copy_link_url`, whose job is to produce something a person can paste.",
        "required": [
          "mode"
        ],
        "properties": {
          "url": {
            "type": "string",
            "description": "Absolute destination URL. ABSENT for `mode: form` — use `form_id`."
          },
          "path": {
            "type": "string",
            "description": "The same destination as this app resolved it — a path for anything served in-app, an absolute URL for an external link. Provided so a native client can route in-app destinations itself instead of re-parsing `url`. ABSENT for `mode: form`."
          },
          "form_id": {
            "type": "integer",
            "description": "The id of the linked form, for `mode: form` only — the locator that replaced this item's form URL. Open your own form screen with it. Absent for every other mode.",
            "example": 583
          },
          "target": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "new_tab",
              "current_tab",
              null
            ],
            "description": "The admin's own choice on the item, which the web anchor's `target` reads. The same column `/search` and `/bookmarks` return as `opens_in`.\nNULLABLE, and a typed client must model it as optional. `library_items.link_target` is `t.string default: \"new_tab\"` with no NOT NULL, the model's enum takes `allow_nil`, and the item form deliberately submits an empty value — which casts back to nil — for every item type whose \"Open link in\" panel is never shown, so an unanswered question stays unanswered rather than being answered \"New tab\" on the admin's behalf. No row holds null today; the first one would crash a client with a non-optional String here."
          },
          "mode": {
            "type": "string",
            "enum": [
              "form",
              "preview",
              "external",
              "in_app"
            ],
            "description": "Which presenter to use, without parsing the URL. `form` — a form this app hosts; route to your form screen using `form_id` (there is no `url`). `preview` — an image or video item with an attached blob, for the file preview / full-screen media viewer. `external` — an absolute URL that belongs in the system browser. `in_app` — a path this app serves itself.\n`preview`, NOT `media`, since 2026-09-05. The card serializer behind `/search` and `/bookmarks` has always spelled this state `preview` in its own `open_mode`; this was the one token of the four where the two surfaces disagreed, so a client switching on the mode string mis-routed an uploaded file depending on which endpoint the item came from.\nThere is no `unavailable` member here, and that is not an omission: where the card's `open_mode` says `unavailable`, this endpoint sends `open: null` instead. Treat a null `open` as \"render the title as plain text\" and read `status` / `status_message` for why.\nOne caveat while the two implementations converge: `preview` is narrower here than on the card. This endpoint requires an `image` or `video` item, so a `file` item carrying its own uploaded PDF answers `in_app` here and `preview` from `/bookmarks` and `/search`. Either way the blob is served by this app — route on `mode` per endpoint, and use `download` / `file` to decide whether there is something to preview at all."
          },
          "description": {
            "type": "string",
            "description": "The actions sheet's sub-label under \"Open\" — per type, and naming the bad state first when there is one (\"Inactive — opens details\").",
            "example": "Opens the file preview"
          }
        }
      },
      "LibraryItemDownload": {
        "type": "object",
        "required": [
          "url",
          "filename"
        ],
        "properties": {
          "url": {
            "type": "string",
            "description": "Absolute blob URL with an attachment disposition."
          },
          "filename": {
            "type": "string",
            "example": "harness-procedure.pdf"
          },
          "format_label": {
            "type": "string",
            "example": "PDF"
          }
        }
      },
      "LibraryItemActions": {
        "type": "object",
        "description": "The ⋯ kebab's option set, ALREADY GATED — so a client renders the menu without re-implementing a single permission rule. Everything above the divider belongs to any viewer; `edit`, `move` and `delete` need `can_manage_items`.\n\"Reorder items\" is deliberately NOT here: it reorders a CATEGORY, so it is answered once as `can_reorder_items` on the category rather than repeated identically on every item in it.\n### Which of these has an API endpoint, and where the rest go\nThis namespace is a READ API with two writes. The complete `/api/v1/libraries` route set is nine entries: `GET /libraries/list`, `GET /libraries/search`, `GET /libraries/bookmarks`, `POST` and `DELETE /libraries/items/{id}/bookmark`, `POST /libraries/{id}/disable`, `POST /libraries/{id}/enable`, `DELETE /libraries/{library_id}/items/{id}`, and this `GET /libraries/{id}`. There is no `PATCH` or `PUT` anywhere, no item-create `POST`, no move and no reorder.\nSo of the eight flags below:\n* `bookmark` and `delete` have a verb —\n  `POST`/`DELETE /libraries/items/{id}/bookmark` and\n  `DELETE /libraries/{library_id}/items/{id}`.\n* `open`, `copy_link`, `download` and `details` need none: they are\n  answered by `open`, `copy_link_url`, `download` and this payload\n  itself.\n* **`edit` and `move` have none.** They are true, correctly-gated\n  answers to \"may this caller do it\", and the action is performed on the\n  web — so the item ships `manage_url`, the absolute URL of the form\n  that performs both. The flag says whether to draw the row;\n  `manage_url` says where the tap goes. Hand it to a web view or the\n  system browser.\n\nThe same split applies to the two `can_reorder_*` flags: neither has an endpoint, `can_reorder_items` hands off through the category's `manage_url`, and `can_reorder_categories` through the library's `link`.\nDo NOT read `edit: true` as \"there is a PATCH for this\". If write endpoints are added later these flags keep their meaning and `manage_url` stays — a URL to the full management screen is worth having next to a narrow write verb — and this section is what will change.",
        "required": [
          "open",
          "bookmark",
          "details",
          "copy_link",
          "download",
          "edit",
          "move",
          "delete"
        ],
        "properties": {
          "open": {
            "type": "boolean",
            "description": "False when `open` is null — there is nowhere to go."
          },
          "bookmark": {
            "type": "boolean",
            "description": "Always true. The toggle is per-user and every viewer holds it."
          },
          "details": {
            "type": "boolean",
            "description": "Always true — the drawer renders from this very payload, so it needs no further call."
          },
          "copy_link": {
            "type": "boolean",
            "description": "False when there is no destination to copy."
          },
          "download": {
            "type": "boolean",
            "description": "True only when the item carries its own uploaded blob."
          },
          "edit": {
            "type": "boolean",
            "description": "Needs `can_manage_items`. NO API endpoint — performed at the item's `manage_url`, which is non-null exactly when this is true."
          },
          "move": {
            "type": "boolean",
            "description": "\"Move to category…\". Needs `can_manage_items` AND somewhere else to move to — asked of the LIBRARY, not of the request, so it stays true under `?category_id=` scoping.\nNO API endpoint, and not a separate screen either: it is performed at the item's `manage_url`, the same edit form as `edit`, whose Category select is the move."
          },
          "delete": {
            "type": "boolean",
            "description": "Needs `can_manage_items`. The one management flag here that DOES have an endpoint — `DELETE /libraries/{library_id}/items/{id}`."
          }
        }
      },
      "LibraryDetailMeta": {
        "type": "object",
        "description": "Facts about THIS RESPONSE, as opposed to about the library. The counts here are response-scoped and narrow with `?category_id=`; their same-named twins on `library` are the library's true totals and do not. Print whichever the surface is actually describing — and never assume the two agree, because under a category scope they deliberately do not.",
        "required": [
          "categories_count",
          "items_count",
          "categories_truncated"
        ],
        "properties": {
          "categories_count": {
            "type": "integer",
            "description": "The number of categories this response carries — so a client never prints a total the payload does not contain. Narrows with `?category_id=`. The library's own total is `library.categories_count`."
          },
          "items_count": {
            "type": "integer",
            "description": "The number of items this response carries, across every category. Response-scoped on the same terms; the library's own total is `library.items_count`.\nCounted per category at no more than `MAX_ITEMS_PER_CATEGORY` (500), so it can never exceed the number of item objects actually serialized even when a category is truncated. Where a category's own `items_count` is its TRUE total and may run ahead of its `items` array, this one deliberately does not — the two answer different questions. No library on any tenant reaches the cap today, so the distinction is dormant."
          },
          "categories_truncated": {
            "type": "boolean",
            "description": "True when the library holds more categories than this response carries — `categories` is capped at 100 (`MAX_CATEGORIES`), and this is what lets a client tell \"this library has 100 categories\" from \"I was given the first 100 of more\".\nNever silently partial: the same contract as `items_truncated` on each category and as `libraries_truncated` / `categories_truncated` on `GET /libraries/search`. The cap is 33× the largest library measured on any tenant (3 categories), so it truncates nothing that exists today — read the flag rather than the measurement.",
            "example": false
          },
          "scoped_to_category_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Echoes `?category_id=` when it was applied, null otherwise — so a client can tell \"this library has one category\" from \"I asked for one category\"."
          }
        }
      },
      "LibraryUserRef": {
        "type": [
          "object",
          "null"
        ],
        "description": "A minimal person reference for a byline.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "example": "Neha Kulkarni"
          }
        }
      },
      "LibrarySearchCounts": {
        "type": "object",
        "description": "Totals per record type, counted over the WHOLE match set rather than the returned page — so a client can label its groups (\"Items 17\") without pagination making the label lie.\nPRESENT AND COMPLETE ON EVERY PAGE, computed off the match relations rather than off the arrays beside it. That is what makes it the answer to \"is `libraries: []` on page 2 an empty result, or a list I already have?\" — and it is also what `libraries_truncated` / `categories_truncated` are derived from. Their sum is `counts_total`.",
        "required": [
          "libraries",
          "categories",
          "items"
        ],
        "properties": {
          "libraries": {
            "type": "integer",
            "example": 1
          },
          "categories": {
            "type": "integer",
            "example": 1
          },
          "items": {
            "type": "integer",
            "example": 16
          }
        }
      },
      "LibrarySearchItemPagination": {
        "type": "object",
        "description": "Describes the ITEM page — the only paginated list in this response. A dedicated schema rather than the shared ResponseMeta, which is the shape of the piggybacked `_meta` envelope (request_id / generated_at / execution_time_ms) and none of whose fields appear here. Same arrangement as the two sibling endpoints (LibraryListMeta, LibraryBookmarkPagination).",
        "required": [
          "total_count",
          "current_page",
          "total_pages",
          "per_page",
          "has_next_page",
          "has_prev_page"
        ],
        "properties": {
          "total_count": {
            "type": "integer",
            "description": "Matching ITEMS across every page — NOT the response's `counts_total`, which is every match of all three record types.",
            "example": 16
          },
          "current_page": {
            "type": "integer",
            "example": 1
          },
          "total_pages": {
            "type": "integer",
            "example": 1
          },
          "per_page": {
            "type": "integer",
            "example": 20
          },
          "has_next_page": {
            "type": "boolean",
            "description": "`current_page < total_pages`. Page until this is false; do not infer the end from a short page.",
            "example": false
          },
          "has_prev_page": {
            "type": "boolean",
            "example": false
          }
        }
      },
      "LibrarySearchCategory": {
        "type": "object",
        "description": "One matching category. Categories have no card elsewhere in this namespace (the detail endpoint nests them inside a library, with their items), so this is the shape a RESULT needs: enough to draw the row and to open the library scoped to that category.",
        "required": [
          "id",
          "name",
          "icon",
          "search_score",
          "library",
          "link"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 42
          },
          "name": {
            "type": "string",
            "example": "People"
          },
          "icon": {
            "type": "string",
            "description": "The category's own Font Awesome class, falling back to `fas fa-folder` — the same fallback the web block and the detail endpoint apply.",
            "example": "fas fa-folder"
          },
          "search_score": {
            "type": "integer",
            "example": 6
          },
          "library": {
            "type": "object",
            "description": "The library this category belongs to, for the row's path line.",
            "required": [
              "id",
              "name",
              "color"
            ],
            "properties": {
              "id": {
                "type": "integer",
                "example": 8
              },
              "name": {
                "type": "string",
                "example": "Company Policies"
              },
              "color": {
                "type": "string",
                "description": "The library's mark colour, with the web card's fallback applied.",
                "example": "#2f64b1"
              }
            }
          },
          "link": {
            "type": "string",
            "format": "uri",
            "description": "ABSOLUTE URL to the library page, anchored at this category's block — the same destination the web result row links to. Absolute because a native client cannot resolve a host-relative path.",
            "example": "https://acme.workforce.mangoapps.com/apps/libraries/spaces/8#category-42"
          }
        }
      },
      "LiveBoard": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "archived"
            ]
          },
          "source_type": {
            "type": "string",
            "description": "Where the board reads from, e.g. `power_table_view` or `recognitions`."
          },
          "sample_data": {
            "type": "boolean",
            "description": "True for a board created by \"Load sample data\"."
          },
          "visualizations_count": {
            "type": "integer",
            "description": "The number of readings IN `visualizations` — i.e. the readings this caller may see, not the board's total."
          },
          "visualizations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LiveBoardReading"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "LiveBoardReading": {
        "type": "object",
        "description": "One reading (visualization) on a board.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "visualization": {
            "type": "string",
            "description": "One of ranked_list, stat, line, table, chart, movers, gauge, status, share, calendar, streak, pace, scorecard."
          },
          "position": {
            "type": "integer"
          },
          "source_name": {
            "type": "string",
            "nullable": true,
            "description": "The source table's name, or null for a native-source reading."
          },
          "data": {
            "$ref": "#/components/schemas/LiveBoardReadingData"
          }
        }
      },
      "LiveBoardReadingData": {
        "type": "object",
        "description": "The reading's current values. Present on the board detail endpoint only. The members below are common to every visualization; the rest of the object varies by type (`entries` for ranked lists and status grids, `value`/`goal` for stats, gauges and pace, `points` for lines, and so on). Masking is already applied.",
        "properties": {
          "viz": {
            "type": "string"
          },
          "period_label": {
            "type": "string",
            "nullable": true
          },
          "source_name": {
            "type": "string",
            "nullable": true
          },
          "source_updated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "source_url": {
            "type": "string",
            "nullable": true,
            "description": "Drill-down into the source table. Present only when this caller could open it."
          },
          "data_version": {
            "type": "integer",
            "nullable": true
          },
          "error": {
            "type": "string",
            "description": "Present INSTEAD of the values when the reading's source is unavailable or its configuration no longer matches it."
          }
        }
      },
      "LiveBoardPlaylist": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "archived"
            ]
          },
          "interval_seconds": {
            "type": "integer",
            "description": "Seconds each board is held on screen (15–600)."
          },
          "board_ids": {
            "type": "array",
            "items": {
              "type": "integer"
            }
          },
          "shared": {
            "type": "boolean",
            "description": "Whether a public rotation link currently exists."
          },
          "rotation_url": {
            "type": "string",
            "nullable": true,
            "description": "App admins only, and null when the rotation is not shared. An unauthenticated URL — treat it as a credential."
          },
          "embed_allowed_origins": {
            "type": "array",
            "description": "App admins only. Sites permitted to frame the rotation. Empty means the rotation is framed only where every one of its boards allows it; if none of them restrict framing, any site may.",
            "items": {
              "type": "string"
            }
          },
          "boards": {
            "type": "array",
            "description": "Detail endpoint only — the rotation's readings, in play order.",
            "items": {
              "$ref": "#/components/schemas/LiveBoardReading"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "NotificationsHomeNeedsYou": {
        "type": "object",
        "description": "Open asks waiting on this user, grouped by subject.",
        "required": [
          "title",
          "ordering",
          "total_count",
          "unread_count",
          "entries"
        ],
        "properties": {
          "title": {
            "type": "string",
            "description": "Section heading, ready to render.",
            "example": "Needs you"
          },
          "ordering": {
            "type": "string",
            "enum": [
              "longest_waiting_first"
            ],
            "description": "The order `entries` is already in — urgent first, then longest waiting first. Render as received rather than re-sorting.\n"
          },
          "total_count": {
            "type": "integer",
            "description": "Open asks across the whole section.",
            "example": 12
          },
          "unread_count": {
            "type": "integer",
            "description": "Unread open asks across the whole section.",
            "example": 5
          },
          "entries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/NotificationsHomeNeedsYouEntry"
            }
          }
        }
      },
      "NotificationsHomeNeedsYouEntry": {
        "type": "object",
        "description": "One row of the Needs-you section: a collapsed subject group (`group: true`) or a single ask that sat below the collapse threshold (`group: false`). Both shapes carry the same keys.\n",
        "required": [
          "group",
          "key",
          "label",
          "icon",
          "count",
          "unread_count",
          "high_priority",
          "oldest_at",
          "oldest_waiting_label"
        ],
        "properties": {
          "group": {
            "type": "boolean",
            "description": "True for a collapsed subject group, false for a single ask. Groups form only at 3+ items in the subject.\n",
            "example": true
          },
          "key": {
            "type": "string",
            "description": "Subject key — pass to `?subject=` to filter to it.",
            "example": "leave_notifications"
          },
          "label": {
            "type": "string",
            "description": "Subject label, as the user's notification settings name it.",
            "example": "Time off"
          },
          "title": {
            "type": "string",
            "nullable": true,
            "description": "The heading to render — the subject label for a group, the notification's own title for a single ask.\n",
            "example": "Time off"
          },
          "icon": {
            "type": "string",
            "description": "Font Awesome icon name, without the `fa-` prefix.",
            "example": "umbrella-beach"
          },
          "count": {
            "type": "integer",
            "description": "Open asks in this entry — renders as the count pill.",
            "example": 4
          },
          "unread_count": {
            "type": "integer",
            "description": "Unread asks in this entry — renders as the \"N new\" pill.",
            "example": 2
          },
          "high_priority": {
            "type": "boolean",
            "description": "True when any ask in the entry is high priority — render the **Urgent** pill.\n",
            "example": true
          },
          "oldest_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the oldest ask in this entry arrived (UTC ISO 8601).",
            "example": "2026-08-19T03:39:20Z"
          },
          "oldest_waiting_label": {
            "type": "string",
            "nullable": true,
            "description": "`oldest_at` as the web phrases it, ready to render after \"oldest waiting\".\n",
            "example": "7 days"
          },
          "notifications": {
            "type": "array",
            "description": "The entry's rows, so expanding costs no second request. A single-ask entry holds exactly one.\n",
            "items": {
              "$ref": "#/components/schemas/Notification"
            }
          }
        }
      },
      "NotificationsHomeActivity": {
        "type": "object",
        "description": "Everything that merely happened, counted per subject and never listed.\n",
        "required": [
          "title",
          "ordering",
          "total_count",
          "unread_count",
          "sections"
        ],
        "properties": {
          "title": {
            "type": "string",
            "example": "Activity"
          },
          "ordering": {
            "type": "string",
            "enum": [
              "largest_first"
            ],
            "description": "The order `sections` is already in — biggest pile first."
          },
          "total_count": {
            "type": "integer",
            "example": 440
          },
          "unread_count": {
            "type": "integer",
            "description": "Drives the \"· N new\" suffix on the section heading.",
            "example": 278
          },
          "sections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/NotificationsHomeActivitySection"
            }
          }
        }
      },
      "NotificationsHomeActivitySection": {
        "type": "object",
        "description": "One Activity subsection. Carries counts only — fetch its rows with `GET /notifications?filter=<key>` when the user expands it — this key is\n    what the Activity group's \"See all →\" opens.\n",
        "required": [
          "key",
          "label",
          "icon",
          "count",
          "unread_count",
          "newest_at"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "Subject key — pass to `?subject=` to list its rows.",
            "example": "company_store"
          },
          "label": {
            "type": "string",
            "example": "Company Store"
          },
          "icon": {
            "type": "string",
            "description": "Font Awesome icon name, without the `fa-` prefix.",
            "example": "store"
          },
          "count": {
            "type": "integer",
            "example": 139
          },
          "unread_count": {
            "type": "integer",
            "description": "Renders as the \"N new\" pill.",
            "example": 139
          },
          "newest_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the latest notification in this subsection was received (UTC ISO 8601).\n",
            "example": "2026-08-26T05:52:17Z"
          },
          "latest_label": {
            "type": "string",
            "nullable": true,
            "description": "`newest_at` in the inbox's short form — \"now\", \"45m\", \"3h\", \"2d\", or \"Mar 4\" beyond a month. Ready to render after \"latest\".\n",
            "example": "3h"
          }
        }
      },
      "Notification": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique notification ID",
            "example": 42
          },
          "title": {
            "type": "string",
            "description": "Notification title",
            "example": "New shift assigned"
          },
          "content": {
            "type": "string",
            "description": "Notification body content",
            "example": "You have been assigned to the morning shift on Feb 20."
          },
          "notification_type": {
            "type": "string",
            "description": "Type/category of the notification",
            "example": "system"
          },
          "read": {
            "type": "boolean",
            "description": "Whether the notification has been read",
            "example": false
          },
          "read_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Timestamp when the notification was read (ISO 8601)",
            "example": "2026-02-19T10:30:00Z"
          },
          "archived": {
            "type": "boolean",
            "description": "Whether the notification is archived",
            "example": false
          },
          "archived_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the notification was archived (ISO 8601), null when it is not archived. This — not created_at — is the sort key for the archived list, so a client holding a cached list can re-sort locally after an archive/unarchive instead of waiting for the next fetch.\n",
            "example": "2026-02-19T11:45:00Z"
          },
          "action_url": {
            "type": "string",
            "nullable": true,
            "description": "Optional URL for the notification action",
            "example": "/shifts/123"
          },
          "source_type": {
            "type": "string",
            "nullable": true,
            "description": "Polymorphic source type",
            "example": "Shift"
          },
          "source_id": {
            "type": "integer",
            "nullable": true,
            "description": "Polymorphic source ID",
            "example": 123
          },
          "metadata": {
            "type": "object",
            "nullable": true,
            "description": "Additional metadata as key-value pairs",
            "example": {}
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Creation timestamp (ISO 8601)",
            "example": "2026-02-19T08:00:00Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "Last update timestamp (ISO 8601)",
            "example": "2026-02-19T10:30:00Z"
          }
        }
      },
      "RecognitionConfig": {
        "type": "object",
        "description": "The tenant + viewer configuration behind `GET /recognitions/config`. Every caller receives every key; the role story lives in the VALUES.",
        "required": [
          "module_enabled",
          "module_label",
          "viewer_role",
          "permissions",
          "features",
          "economy",
          "limits",
          "default_visibility",
          "visibility_options",
          "values",
          "tags",
          "cards",
          "programs"
        ],
        "properties": {
          "module_enabled": {
            "type": "boolean",
            "description": "Is Recognize on for this org AND visible to this viewer. Always true in a 200 — the endpoint 403s otherwise — and reported so a client can cache one settings shape.",
            "example": true
          },
          "module_label": {
            "type": "string",
            "description": "The app's display name. Terminology varies per org, and the name lives on the app record, so a console rename reaches the native shell with no client release.",
            "example": "Recognitions"
          },
          "viewer_role": {
            "type": "string",
            "enum": [
              "employee",
              "manager",
              "admin"
            ],
            "description": "`manager` = has direct reports. `admin` = a business admin/owner or a Recognitions app-admin, and outranks `manager` when the viewer is both. Drives the role-adaptive footer, the More menu and Team gating.",
            "example": "manager"
          },
          "permissions": {
            "type": "object",
            "description": "What THIS viewer may do. Each boolean is the same guard the corresponding write path enforces, so a control rendered from one of these is a control whose POST will be accepted.",
            "required": [
              "can_give",
              "can_nominate",
              "can_boost",
              "can_react",
              "can_approve",
              "can_quick_award",
              "can_moderate"
            ],
            "properties": {
              "can_give": {
                "type": "boolean",
                "description": "May give peer recognition at all. False when the tenant switched peer recognition off and the viewer is neither a manager nor an admin.",
                "example": true
              },
              "can_nominate": {
                "type": "boolean",
                "description": "May file a nomination in AT LEAST ONE listed program right now. False means the picker would be empty — hide the entry point. Also false whenever award requests (Model A) are off.",
                "example": true
              },
              "can_boost": {
                "type": "boolean",
                "description": "Whether the Boost affordance exists for this viewer — the tenant has peer points on AND this viewer may give. Per-post eligibility (not your own give, not already boosted, enough allowance left) rides on each post's own `boost` block.",
                "example": false
              },
              "can_react": {
                "type": "boolean",
                "description": "The tenant's reactions switch. Defaults on.",
                "example": true
              },
              "can_approve": {
                "type": "boolean",
                "description": "May decide nominations and held posts — the reviewer rule the approvals queue and the decision endpoints enforce.",
                "example": true
              },
              "can_quick_award": {
                "type": "boolean",
                "description": "May give an instant manager award with no approval step. Requires BOTH the tenant's Quick Award switch and reviewer standing. The service still refuses an award to someone who is not a direct report — that check is per-recipient.",
                "example": true
              },
              "can_moderate": {
                "type": "boolean",
                "description": "May remove or revoke SOMEONE ELSE'S recognition. An author may always remove their own give; that is per-record and is reported on the item itself.",
                "example": false
              }
            }
          },
          "features": {
            "type": "object",
            "description": "The TENANT switches that decide which surfaces exist, separate from `permissions`, which decide what this viewer may do with them.",
            "properties": {
              "award_requests_enabled": {
                "type": "boolean",
                "description": "Model A — ad-hoc award requests (nominate → approve → award). Defaults on.",
                "example": true
              },
              "award_cycles_enabled": {
                "type": "boolean",
                "description": "Model B — time-boxed award cycles. Defaults off.",
                "example": false
              },
              "quick_award_enabled": {
                "type": "boolean",
                "example": true
              },
              "comments_enabled": {
                "type": "boolean",
                "example": true
              },
              "reactions_enabled": {
                "type": "boolean",
                "example": true
              },
              "tags_enabled": {
                "type": "boolean",
                "description": "When false, `tags` is empty and the composer should hide the tag picker rather than rendering an empty one.",
                "example": true
              },
              "anonymous_allowed": {
                "type": "boolean",
                "description": "Whether `anonymous` appears in `visibility_options`. Defaults OFF — recognition is attributed by default.",
                "example": false
              }
            }
          },
          "economy": {
            "type": "object",
            "description": "The points economy, as this tenant configured it. All figures are zero while `points_enabled` is false, so a client never advertises a pot that cannot be spent.",
            "properties": {
              "points_enabled": {
                "type": "boolean",
                "description": "Peer redeemable points. Requires BOTH the admin toggle and a working Company Store integration — the give path refuses on either.",
                "example": true
              },
              "wallet_allowance": {
                "type": "integer",
                "description": "THIS viewer's monthly \"points to give\" allowance.",
                "example": 1000
              },
              "wallet_remaining": {
                "type": "integer",
                "description": "What is left of it this month. Resets monthly; does not carry over.",
                "example": 750
              },
              "wallet_tier": {
                "type": "string",
                "enum": [
                  "ic",
                  "manager",
                  "leadership"
                ],
                "description": "Which allowance band this viewer falls in.",
                "example": "ic"
              },
              "manager_allowance": {
                "type": "integer",
                "description": "The MANAGER band's configured monthly allowance, regardless of this viewer's own band — what a manager-facing screen budgets against. Reported to every viewer so one shell renders for all.",
                "example": 2000
              },
              "boost_tiers": {
                "type": "array",
                "description": "The pile-on amounts a Boost offers. The server rejects anything else. A post's own `boost` block narrows these to what the viewer's remaining allowance covers.",
                "items": {
                  "type": "integer"
                },
                "example": [
                  5,
                  10,
                  25
                ]
              },
              "points_per_dollar": {
                "type": "integer",
                "description": "The store's conversion rate. The economy is STORED in dollars and PRESENTED in points; use this rather than assuming 100.",
                "example": 100
              },
              "point_tiers": {
                "type": "array",
                "description": "The give form's points selector, in order, always leading with the 0-point shout-out so \"no points\" is an offered choice rather than an empty field.",
                "items": {
                  "type": "object",
                  "properties": {
                    "points": {
                      "type": "integer",
                      "example": 250
                    },
                    "label": {
                      "type": "string",
                      "example": "250 pts"
                    }
                  }
                }
              }
            }
          },
          "limits": {
            "type": "object",
            "description": "What the server will actually accept — read from the constants the validations and the group fan-out are written from.",
            "properties": {
              "max_recipients": {
                "type": "integer",
                "description": "The most people ONE give may name.",
                "example": 50
              },
              "message_min": {
                "type": "integer",
                "example": 10
              },
              "message_max": {
                "type": "integer",
                "example": 1000
              },
              "nomination_title_min": {
                "type": "integer",
                "example": 2
              },
              "nomination_title_max": {
                "type": "integer",
                "example": 100
              },
              "nomination_description_min": {
                "type": "integer",
                "example": 10
              },
              "nomination_description_max": {
                "type": "integer",
                "example": 1000
              },
              "nomination_justification_max": {
                "type": "integer",
                "example": 2000
              },
              "nomination_supporting_files_max_count": {
                "type": "integer",
                "description": "Attachments one nomination may carry.",
                "example": 5
              },
              "nomination_supporting_file_max_bytes": {
                "type": "integer",
                "description": "Per-file size cap, in bytes.",
                "example": 10485760
              },
              "nomination_supporting_file_extensions": {
                "type": "array",
                "description": "The extensions the uploader accepts. Use these for the file picker's filter rather than hardcoding a list.",
                "items": {
                  "type": "string"
                },
                "example": [
                  ".pdf",
                  ".png",
                  ".jpg",
                  ".docx",
                  ".xlsx",
                  ".csv",
                  ".txt"
                ]
              },
              "nomination_supporting_file_types_label": {
                "type": "string",
                "description": "The same set as human copy, for the uploader's hint line.",
                "example": "PDF, Word, Excel, CSV, TXT, PNG, JPG, GIF or WEBP"
              }
            }
          },
          "default_visibility": {
            "type": "string",
            "enum": [
              "public",
              "private"
            ],
            "description": "The composer's untouched default. A tenant configured non-public defaults its gives to `private` — and only then is `private` offered.",
            "example": "public"
          },
          "visibility_options": {
            "type": "array",
            "description": "The composer's visibility AND anonymity toggles, filtered to what this org allows, in composer order.",
            "items": {
              "type": "object",
              "properties": {
                "value": {
                  "type": "string",
                  "enum": [
                    "public",
                    "department",
                    "private",
                    "anonymous"
                  ],
                  "description": "The wire value. `department` is the prototype's \"my_department\". The legacy `team` synonym is never offered.",
                  "example": "department"
                },
                "param": {
                  "type": "string",
                  "enum": [
                    "visibility",
                    "is_anonymous"
                  ],
                  "description": "Which request parameter this option is submitted under — `anonymous` is the separate `is_anonymous` boolean, not a `visibility` value.",
                  "example": "visibility"
                },
                "label": {
                  "type": "string",
                  "example": "My Department"
                },
                "description": {
                  "type": "string",
                  "example": "Visible to people in your department"
                }
              }
            }
          },
          "values": {
            "type": "array",
            "description": "The tenant's active company values, in picker order. Giving points REQUIRES one, so a brand-new tenant is seeded with a starter set on first read rather than being shown an empty picker.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 12
                },
                "name": {
                  "type": "string",
                  "example": "Customer First"
                },
                "slug": {
                  "type": "string",
                  "example": "customer-first"
                },
                "description": {
                  "type": "string",
                  "nullable": true,
                  "example": "Puts customers at the center of every decision"
                },
                "icon": {
                  "type": "string",
                  "nullable": true,
                  "description": "Admin-set; fall back to your own neutral chip when null.",
                  "example": "fas fa-handshake"
                },
                "color": {
                  "type": "string",
                  "nullable": true,
                  "example": "#0d6efd"
                }
              }
            }
          },
          "tags": {
            "type": "array",
            "description": "The tag chips the composer offers. An admin-curated list is used verbatim; otherwise the tenant's own most-used tags of the last 30 days topped up with platform defaults. Empty when `tags_enabled` is false.",
            "items": {
              "type": "string"
            },
            "example": [
              "teamwork",
              "excellence",
              "innovation"
            ]
          },
          "cards": {
            "type": "object",
            "description": "The award-card gallery, split by where each design comes from.",
            "properties": {
              "tenant": {
                "type": "array",
                "description": "Tenant-authored cards. Only designs that actually have art are listed.",
                "items": {
                  "$ref": "#/components/schemas/RecognitionAwardCard"
                }
              },
              "gallery": {
                "type": "array",
                "description": "Central-gallery cards (the catalog also shown at mangoapps.com/templates/recognition). Empty — never an error — when the gallery is unreachable.",
                "items": {
                  "$ref": "#/components/schemas/RecognitionAwardCard"
                }
              },
              "gallery_truncated": {
                "type": "boolean",
                "description": "True when the gallery page came back FULL: the catalog may hold designs this payload never listed. Say so in a picker's \"no match\" state rather than letting a search miss read as \"that card doesn't exist\".",
                "example": false
              }
            }
          },
          "programs": {
            "type": "object",
            "description": "The nominate picker's vocabulary.",
            "properties": {
              "items": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/RecognitionConfigProgram"
                }
              },
              "total_count": {
                "type": "integer",
                "description": "The tenant's FULL nominatable-program count, so a client can say \"3 of 12\" and link to the paginated Programs screen.",
                "example": 3
              },
              "truncated": {
                "type": "boolean",
                "description": "Whether more programs exist than this payload listed.",
                "example": false
              }
            }
          }
        }
      },
      "RecognitionAwardCard": {
        "type": "object",
        "description": "One award-card design for the give composer's picker. Submit `award_template_id` VERBATIM — it is an integer for a tenant card and the string `\"central:<slug>\"` for a gallery card.",
        "properties": {
          "source": {
            "type": "string",
            "enum": [
              "tenant",
              "gallery"
            ],
            "example": "tenant"
          },
          "id": {
            "type": "integer",
            "nullable": true,
            "description": "The local record id. Null for a gallery card, which has none.",
            "example": 8
          },
          "award_template_id": {
            "oneOf": [
              {
                "type": "integer"
              },
              {
                "type": "string"
              }
            ],
            "description": "The value POST /recognition/give takes for this card.",
            "example": 8
          },
          "name": {
            "type": "string",
            "example": "Star Performer"
          },
          "slug": {
            "type": "string",
            "nullable": true,
            "example": "star-performer"
          },
          "default_message": {
            "type": "string",
            "nullable": true,
            "description": "The message the composer pre-fills when this card is picked. Tenant cards only.",
            "example": "You went above and beyond."
          },
          "default_points": {
            "type": "integer",
            "nullable": true,
            "example": 250
          },
          "category": {
            "type": "string",
            "nullable": true,
            "description": "Tenant cards only — the recognition category this design belongs to.",
            "example": "Above & Beyond"
          },
          "category_id": {
            "type": "integer",
            "nullable": true,
            "example": 4
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "Gallery cards only.",
            "example": "A simple thanks."
          },
          "art_url": {
            "type": "string",
            "nullable": true,
            "description": "Absolute URL to the card art.",
            "example": "https://acme.workforce.mangoapps.com/rails/active_storage/blobs/star.png"
          }
        }
      },
      "RecognitionConfigProgram": {
        "type": "object",
        "description": "One nominatable program, trimmed to what a NOMINATE PICKER needs. The full card — points-remaining bar, per-viewer counts, window dates — is served by `GET /recognitions/programs`.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 3
          },
          "name": {
            "type": "string",
            "example": "Above & Beyond"
          },
          "slug": {
            "type": "string",
            "nullable": true,
            "example": "above-and-beyond"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "example": "For work that goes well past the brief."
          },
          "program_type": {
            "type": "string",
            "example": "peer_to_peer"
          },
          "program_type_label": {
            "type": "string",
            "example": "Peer to Peer"
          },
          "can_nominate": {
            "type": "boolean",
            "description": "Whether this viewer may file a nomination RIGHT NOW — the same answer the submit path gives. Hide the entry rather than disabling it.",
            "example": true
          },
          "nomination_block_reason": {
            "type": "string",
            "nullable": true,
            "description": "Why not, in the viewer's own words. Null when they can. Render it — a disabled row with no reason and no next step is a dead end.",
            "example": "Quarterly Star Award is a Manager Recognition program — only managers can nominate in it. Recognize a teammate through a peer-to-peer program instead."
          },
          "categories": {
            "type": "array",
            "description": "The program's active categories, in sort order.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 9
                },
                "name": {
                  "type": "string",
                  "example": "Customer Impact"
                },
                "slug": {
                  "type": "string",
                  "example": "customer-impact"
                },
                "icon": {
                  "type": "string",
                  "example": "star"
                },
                "color": {
                  "type": "string",
                  "example": "#0d6efd"
                }
              }
            }
          }
        }
      },
      "RecognitionGiveRequest": {
        "type": "object",
        "description": "The give composer's fields, one per control on the screen. Every option catalog behind them is served by `GET /recognitions/config`, so a client renders the picker from the same source the server validates against.",
        "properties": {
          "recipient_ids": {
            "type": "array",
            "description": "The people to recognize, in the order the client picked them, up to `limits.max_recipients` (50). Only people `GET /recognitions/employee_suggestions` offers are acceptable — active members of this business, service / AI-agent principals excluded. The caller's own id is dropped rather than refused.",
            "items": {
              "type": "integer"
            },
            "example": [
              812,
              907
            ]
          },
          "recipient_id": {
            "type": "integer",
            "nullable": true,
            "description": "Single-recipient form, for a one-person give and for the \"Recognize <person>\" deep link. Ignored when `recipient_ids` is present.",
            "example": 812
          },
          "content": {
            "type": "string",
            "description": "The recognition message — 10..1000 characters (`limits.message_min` / `message_max`). May be blank ONLY when `award_template_id` names a card that carries a default message, which then fills it.",
            "minLength": 10,
            "maxLength": 1000,
            "example": "You carried the migration all weekend — thank you."
          },
          "award_template_id": {
            "type": "string",
            "nullable": true,
            "description": "The \"Pick a card\" selection, taken verbatim from `config.cards[].award_template_id`: a tenant card's integer id (as a string or a number), or `\"central:<slug>\"` for a gallery card. An unknown id, a stale slug or a gallery outage is not an error — the give simply posts without a card.",
            "example": "central:thank-you-star"
          },
          "points": {
            "type": "integer",
            "minimum": 0,
            "default": 0,
            "description": "Reward points from the giver's own monthly allowance — one of `economy.point_tiers`. `0` is a plain shout-out. Requires `company_value_id` when positive. Silently 0 when the tenant has peer points switched off.",
            "example": 250
          },
          "company_value_id": {
            "type": "integer",
            "nullable": true,
            "description": "The core value this recognition is tied to (one of `config.values`). REQUIRED once `points` is positive.",
            "example": 4
          },
          "recognition_tags": {
            "type": "array",
            "description": "Informal tags from `config.tags`. The first 5 are kept. A comma-separated string is accepted too.",
            "items": {
              "type": "string"
            },
            "example": [
              "teamwork",
              "above-and-beyond"
            ]
          },
          "visibility": {
            "type": "string",
            "enum": [
              "public",
              "department",
              "private"
            ],
            "nullable": true,
            "description": "One of `config.visibility_options`. **Omit it** to take the tenant's own default — do not send `public` as a fallback, which is how a private-by-default tenant's gives end up published publicly.",
            "example": "department"
          },
          "is_anonymous": {
            "type": "boolean",
            "default": false,
            "description": "Post without the giver's name. Forced false unless `features.anonymous_allowed` is true — anonymity is opt-in per tenant.",
            "example": false
          },
          "photo_url": {
            "type": "string",
            "format": "uri",
            "nullable": true,
            "description": "A photo for this recognition, named by URL — the JSON equivalent of the web composer's file upload. Must be a **public HTTPS** URL serving a JPG, PNG, WebP or HEIC image under 10 MB; the format is decided by sniffing the bytes, not by the `Content-Type` header or the extension. The server fetches it inline and stores the bytes, so the response card's `photo_url` is OUR rendition and this link is never referenced again — a URL behind a login, a signed URL that expires, or one on a private network will not work. Anything unusable refuses the give with `invalid_photo_url` (422) and writes nothing. A group give stores one copy, shared by every row.",
            "example": "https://cdn.example.com/uploads/install-day.jpg"
          }
        }
      },
      "RecognitionGiveResult": {
        "type": "object",
        "description": "The created recognition, what the give actually did, and the caller's remaining allowance.",
        "required": [
          "recognition",
          "recognition_ids",
          "status",
          "message"
        ],
        "properties": {
          "recognition": {
            "$ref": "#/components/schemas/RecognitionGiveCard"
          },
          "recognition_ids": {
            "type": "array",
            "description": "Every row the submission wrote — one per recipient. The card above collapses a group give into one row (as the feed does), so this is how a client addresses an individual row later, to open or delete it.",
            "items": {
              "type": "integer"
            },
            "example": [
              4473,
              4474
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "posting",
              "pending_review",
              "pending_approval"
            ],
            "description": "What the give DID. `active` is live; `posting` is held for the automated content screen (the normal AI-moderation path) and publishes moments later; `pending_review` is a keyword/policy hold; `pending_approval` is routed to the recipient's manager. **Never assume a 201 means live.**",
            "example": "active"
          },
          "message": {
            "type": "string",
            "description": "The web flash for this `status`, word for word — safe to show as-is.",
            "example": "Recognition for Priya Nair has been shared!"
          },
          "giving_remaining": {
            "type": "integer",
            "description": "The caller's remaining monthly points-to-give allowance AFTER this give, so a wallet header updates without a second request. 0 when peer points are off.",
            "example": 750
          },
          "unread_notification_count": {
            "type": "integer",
            "example": 3
          }
        }
      },
      "RecognitionGiveCard": {
        "type": "object",
        "description": "The created recognition as the FEED renders it — the same card shape `GET /recognitions/feed` returns, so a client can insert it optimistically and have the next refresh agree. A brand-new give has no reactions, comments or boosts, and its own author can never boost it.",
        "required": [
          "type",
          "kind",
          "id",
          "status",
          "outcome_status"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "recognition_post"
            ],
            "example": "recognition_post"
          },
          "kind": {
            "type": "string",
            "enum": [
              "recognition"
            ],
            "example": "recognition"
          },
          "id": {
            "type": "integer",
            "example": 4471
          },
          "title": {
            "type": "string",
            "nullable": true
          },
          "message": {
            "type": "string",
            "nullable": true,
            "description": "The recognition body — the giver's message, or the card's default copy when they left it blank."
          },
          "points": {
            "type": "integer",
            "example": 250
          },
          "recipient": {
            "$ref": "#/components/schemas/RecognitionPersonCard"
          },
          "group_recipients": {
            "type": "array",
            "nullable": true,
            "description": "Every recipient of the submission, for a group give. Always present and **null** for a single-recipient give — the field never disappears, so a client reads it the same way here, on the feed card and on the detail screen.",
            "items": {
              "$ref": "#/components/schemas/RecognitionPersonCard"
            }
          },
          "giver": {
            "allOf": [
              {
                "$ref": "#/components/schemas/RecognitionPersonCard"
              }
            ],
            "nullable": true,
            "description": "The giver. **Null for an anonymous give**, even for the caller who just made it."
          },
          "anonymous": {
            "type": "boolean",
            "example": false
          },
          "program": {
            "type": "string",
            "nullable": true
          },
          "category": {
            "type": "string",
            "nullable": true
          },
          "company_value": {
            "type": "string",
            "nullable": true,
            "example": "Customer First"
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time",
            "description": "When it was published, falling back to when it was created — a give held for the content screen has no publish time yet but still sorts."
          },
          "visibility": {
            "type": "string",
            "example": "public"
          },
          "status": {
            "type": "string",
            "description": "The persisted column. A give awaiting the content screen reads `pending_review` here — branch on `outcome_status` (or the top-level `status`) instead, which tells `posting` apart from a real moderator hold.",
            "example": "active"
          },
          "outcome_status": {
            "type": "string",
            "enum": [
              "active",
              "posting",
              "pending_review",
              "pending_approval"
            ],
            "description": "The same value as the response's top-level `status`, repeated on the card.",
            "example": "active"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "photo_url": {
            "type": "string",
            "nullable": true,
            "description": "The photo the give carried, as the stored WebP rendition — the same value `GET /recognitions/feed` and `GET /recognitions/posts/{id}` report, so the card can be inserted optimistically. Always present, **null** when the give sent no `photo_url`. On a group give every row shares this one image."
          },
          "award_art_url": {
            "type": "string",
            "nullable": true,
            "description": "Art of the card that was attached. For a gallery card this is a freshly materialized tenant asset, so the client cannot know it in advance."
          },
          "boost": {
            "$ref": "#/components/schemas/RecognitionBoost"
          }
        }
      },
      "RecognitionGiveError": {
        "type": "object",
        "description": "A refused give. `error.code` is the rule that refused it.",
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "no_recipients",
                  "invalid_recipients",
                  "too_many_recipients",
                  "content_missing",
                  "invalid_photo_url",
                  "governance_blocked",
                  "content_rejected",
                  "invalid",
                  "giving_not_allowed",
                  "access_denied"
                ]
              },
              "message": {
                "type": "string",
                "description": "Ready to show. For `governance_blocked` this is the guard's own sentence, which names the limit and when it lifts."
              },
              "details": {
                "type": "object",
                "nullable": true,
                "description": "Present on the two refusals a client can act on structurally: `max_recipients` for `too_many_recipients`, `recipient_ids` for `invalid_recipients`."
              }
            }
          }
        }
      },
      "RecognitionDeletion": {
        "type": "object",
        "description": "The result of the ⋯ ▸ Delete action, returned by both `DELETE /recognitions/posts/{id}` (soft delete) and `DELETE /recognitions/awards/{id}` (admin revoke). `type` says which table the recognition came from and `status` the past tense it landed in, so a client can render the confirmation without re-deriving either.",
        "required": [
          "id",
          "type",
          "deleted",
          "status",
          "already_deleted"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 4471,
            "description": "The removed recognition's id."
          },
          "type": {
            "type": "string",
            "enum": [
              "recognition_post",
              "award"
            ],
            "description": "`recognition_post` for a peer give, `award` for an award or automated certificate."
          },
          "deleted": {
            "type": "boolean",
            "example": true,
            "description": "Always true on a 200 — the recognition is no longer live."
          },
          "status": {
            "type": "string",
            "enum": [
              "deleted",
              "revoked"
            ],
            "description": "The row's new status — `deleted` for a soft-deleted post, `revoked` for an award."
          },
          "already_deleted": {
            "type": "boolean",
            "example": false,
            "description": "True when the recognition was ALREADY removed and this request was a no-op (posts only — a re-issued award revoke answers 422 `not_revocable`). Points are never reversed twice."
          },
          "message": {
            "type": "string",
            "nullable": true,
            "example": "Recognition removed.",
            "description": "A ready-to-show confirmation, worded as the web words it."
          },
          "unread_notification_count": {
            "type": "integer",
            "example": 3
          }
        }
      },
      "RecognitionEdit": {
        "type": "object",
        "description": "The result of the ⋯ ▸ Edit action, returned by both `PATCH /recognitions/posts/{id}` and `PATCH /recognitions/awards/{id}`. `recognition` is the SAME card the matching `GET` returns — one serializer, so a client replaces its row from this response instead of re-fetching, and there is no edit-only shape to keep in step.",
        "required": [
          "recognition",
          "unchanged"
        ],
        "properties": {
          "recognition": {
            "$ref": "#/components/schemas/RecognitionDetail"
          },
          "unchanged": {
            "type": "boolean",
            "example": false,
            "description": "True when the body matched what was already stored: nothing was written, no moderation screen ran and no notification fired. A client can skip its \"saved\" animation on this."
          },
          "message": {
            "type": "string",
            "nullable": true,
            "example": "Recognition updated.",
            "description": "A ready-to-show confirmation, worded as the web words it."
          },
          "unread_notification_count": {
            "type": "integer",
            "example": 3
          }
        }
      },
      "RecognitionComment": {
        "type": "object",
        "description": "ONE comment on a recognition post or award. Threading is ONE level deep: a top-level comment carries its direct replies inline in `replies`, and a serialized reply always has `parent_id` set and `replies: []`. Only ACTIVE replies are inlined, so a reply held for moderation or removed never reaches a client. Reactions are summarized; `can_edit`/`can_delete` are per-viewer and match what the write endpoints enforce.",
        "required": [
          "id",
          "item_type",
          "item_id",
          "content",
          "status",
          "created_at",
          "can_edit",
          "can_delete"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 991
          },
          "parent_id": {
            "type": "integer",
            "nullable": true,
            "example": 990,
            "description": "null on a top-level comment; the id of the comment this one replies to otherwise. Same key POST accepts."
          },
          "item_type": {
            "type": "string",
            "enum": [
              "recognition_post",
              "award"
            ],
            "description": "The canonical commentable type of the parent recognition."
          },
          "item_id": {
            "type": "integer",
            "example": 4471,
            "description": "The parent recognition's id."
          },
          "author": {
            "allOf": [
              {
                "$ref": "#/components/schemas/RecognitionPersonCard"
              }
            ],
            "nullable": true,
            "description": "The comment's author (id, name, title, image)."
          },
          "content": {
            "type": "string",
            "example": "Fantastic work — well deserved!"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "hidden",
              "deleted"
            ],
            "description": "`active` normally; `hidden` when held by moderation; `deleted` after removal."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "edited_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the author last edited the CONTENT (nil if never edited; unaffected by moderation status flips)."
          },
          "edited": {
            "type": "boolean",
            "description": "True iff `edited_at` is present."
          },
          "time_ago": {
            "type": "string",
            "nullable": true,
            "example": "about 2 hours ago"
          },
          "replies_count": {
            "type": "integer",
            "example": 2,
            "description": "How many ACTIVE replies are inlined below. Always 0 on a reply."
          },
          "replies": {
            "type": "array",
            "description": "The comment's ACTIVE direct replies, oldest-first. Always empty on a reply — threads are one level deep — so this recursion terminates after one hop.",
            "items": {
              "$ref": "#/components/schemas/RecognitionComment"
            }
          },
          "reactions": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer",
                "example": 3
              },
              "top_emojis": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "emoji": {
                      "type": "string",
                      "example": "👍"
                    },
                    "count": {
                      "type": "integer",
                      "example": 2
                    }
                  }
                }
              },
              "my_reactions": {
                "type": "array",
                "items": {
                  "type": "string",
                  "example": "❤️"
                },
                "description": "The viewer's own emojis on this comment."
              }
            }
          },
          "can_edit": {
            "type": "boolean",
            "description": "Whether THIS caller may edit the comment (author only)."
          },
          "can_delete": {
            "type": "boolean",
            "description": "Whether THIS caller may delete the comment (author or a Recognitions moderator)."
          }
        }
      },
      "RecognitionCommentPageMeta": {
        "type": "object",
        "required": [
          "current_page",
          "per_page",
          "total_count",
          "total_pages",
          "has_next_page",
          "has_prev_page"
        ],
        "properties": {
          "current_page": {
            "type": "integer",
            "example": 1
          },
          "per_page": {
            "type": "integer",
            "example": 20
          },
          "total_count": {
            "type": "integer",
            "example": 42,
            "description": "Total ACTIVE TOP-LEVEL comments on the recognition — the rows this list pages over. Inlined replies are NOT counted here; the detail endpoint's `comments_count` counts every visible row instead."
          },
          "total_pages": {
            "type": "integer",
            "example": 3
          },
          "has_next_page": {
            "type": "boolean",
            "example": true
          },
          "has_prev_page": {
            "type": "boolean",
            "example": false
          }
        }
      },
      "RecognitionDetail": {
        "type": "object",
        "description": "ONE recognition rendered for the detail screen — a peer post, an award, or an automated certificate. Shared verbatim between /recognitions/posts/{id} and /recognitions/awards/{id}; `type` and `kind` say which was resolved.",
        "required": [
          "type",
          "kind",
          "id",
          "engagement",
          "permissions"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "recognition_post",
              "award"
            ],
            "description": "The persisted model — `recognition_post` or `award`."
          },
          "kind": {
            "type": "string",
            "enum": [
              "recognition",
              "award",
              "certificate"
            ],
            "description": "The user-facing kind shown in the feed. `recognition` = peer post, `award` = human-given award, `certificate` = automated award."
          },
          "id": {
            "type": "integer",
            "example": 4471
          },
          "title": {
            "type": "string",
            "nullable": true,
            "example": "Great Work"
          },
          "message": {
            "type": "string",
            "nullable": true,
            "description": "The recognition body (post content or award description/citation).",
            "example": "You carried the migration all weekend — thank you."
          },
          "points": {
            "type": "integer",
            "example": 40,
            "description": "The recognition's worth in reward points."
          },
          "recipient": {
            "$ref": "#/components/schemas/RecognitionPersonCard"
          },
          "group_recipients": {
            "type": "array",
            "nullable": true,
            "description": "Present only for a grouped give (one submission → many recipients).",
            "items": {
              "$ref": "#/components/schemas/RecognitionPersonCard"
            }
          },
          "giver": {
            "allOf": [
              {
                "$ref": "#/components/schemas/RecognitionPersonCard"
              }
            ],
            "nullable": true,
            "description": "The giver. Null for an anonymous give; \"System (Automated)\" (id null) for an automated certificate."
          },
          "anonymous": {
            "type": "boolean",
            "example": false
          },
          "program": {
            "type": "string",
            "nullable": true,
            "example": "Spot Award"
          },
          "category": {
            "type": "string",
            "nullable": true,
            "example": "Teamwork"
          },
          "company_value": {
            "type": "string",
            "nullable": true,
            "example": "Customer First"
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "visibility": {
            "type": "string",
            "nullable": true,
            "description": "Post visibility (public/team/department/private) or public/private for an award.",
            "example": "public"
          },
          "status": {
            "type": "string",
            "nullable": true,
            "example": "active"
          },
          "automated": {
            "type": "boolean",
            "description": "True for a system-generated certificate; always false for a post.",
            "example": false
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Recognition tags (posts only; empty for awards)."
          },
          "photo_url": {
            "type": "string",
            "nullable": true,
            "description": "Attached photo (posts only)."
          },
          "award_art_url": {
            "type": "string",
            "nullable": true,
            "description": "Award-card art (posts with a card) or the certificate page (awards)."
          },
          "engagement": {
            "$ref": "#/components/schemas/RecognitionEngagement"
          },
          "boost": {
            "allOf": [
              {
                "$ref": "#/components/schemas/RecognitionBoost"
              }
            ],
            "nullable": true,
            "description": "Pile-on points block. Present for posts; null for awards (not boostable)."
          },
          "permissions": {
            "$ref": "#/components/schemas/RecognitionPermissions"
          }
        }
      },
      "RecognitionQuickAwardForm": {
        "type": "object",
        "description": "Everything the Quick Award form renders from. Persona-aware but not persona-branched: every reviewer receives the same keys, and what varies is the values — `recipient_scope`, the budget, and the recent-awards list.",
        "required": [
          "enabled",
          "require_category",
          "recipient_scope",
          "amount",
          "limits",
          "programs",
          "budget",
          "recent_awards"
        ],
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "Always `true` in a 200 — the endpoint answers 403 `feature_disabled` otherwise. Reported so one client model covers both answers.",
            "example": true
          },
          "require_category": {
            "type": "boolean",
            "description": "Whether a category is mandatory (tenant setting `quick_award_require_category`). Render the required marker from this; the POST enforces it with 422 `category_required`.",
            "example": false
          },
          "recipient_scope": {
            "type": "string",
            "enum": [
              "all",
              "direct_reports"
            ],
            "description": "Who this caller may award, and therefore the `scope` to call `GET /recognitions/employee_suggestions` with. `all` for a Recognitions admin; `direct_reports` for a manager — the same rule the POST applies to `recipient_id`.",
            "example": "direct_reports"
          },
          "amount": {
            "$ref": "#/components/schemas/RecognitionQuickAwardAmount"
          },
          "limits": {
            "$ref": "#/components/schemas/RecognitionQuickAwardLimits"
          },
          "default_program_id": {
            "type": "integer",
            "nullable": true,
            "description": "What \"Default Program\" resolves to, and the program the POST funds the award from when `program_id` is omitted — so the `budget` below is the budget a default award actually draws down. Null only when the tenant has no active program at all, in which case the POST answers 422 `no_program`.",
            "example": 7
          },
          "programs": {
            "type": "array",
            "description": "Active, in-window programs in name order — the web picker's own list. Empty when the tenant has none.",
            "items": {
              "$ref": "#/components/schemas/RecognitionQuickAwardProgram"
            }
          },
          "budget": {
            "$ref": "#/components/schemas/RecognitionQuickAwardBudget"
          },
          "recent_awards": {
            "type": "array",
            "description": "This manager's most recent awards in THIS tenant, newest first, capped at 5 — the web sidebar's confirmation list. Finalized only: revoked and expired awards are excluded, matching every count of \"recognition given\". Deliberately smaller than a feed card; the full card is at `GET /recognitions/awards/{id}`.",
            "items": {
              "$ref": "#/components/schemas/RecognitionQuickAwardRecent"
            }
          }
        }
      },
      "RecognitionQuickAwardAmount": {
        "type": "object",
        "description": "The amount input's bounds, **all in reward points**. The columns store dollars (1 pt = 1¢); the server converts both ways, so a client never handles dollars.",
        "required": [
          "min_points",
          "max_points",
          "default_points",
          "presets",
          "points_per_dollar"
        ],
        "properties": {
          "min_points": {
            "type": "integer",
            "description": "The floor. A zero or negative amount is refused `invalid_amount`.",
            "example": 1
          },
          "max_points": {
            "type": "integer",
            "description": "The tenant's per-award cap (`quick_award_max_amount`). Never widen the input past this — the POST refuses it with `amount_over_limit`. A program's own `per_award_limit_points` and a category's range can be tighter still.",
            "example": 10000
          },
          "default_points": {
            "type": "integer",
            "description": "What to pre-fill (`quick_award_default_amount`), already clamped to `max_points` so it is never an amount the server would refuse.",
            "example": 2500
          },
          "presets": {
            "type": "array",
            "description": "The web \"Quick select\" ladder, already filtered to the cap — offered so a phone renders the same one-tap amounts instead of inventing its own. Falls back to `[max_points]` for a tenant whose cap is below the smallest rung.",
            "items": {
              "type": "integer"
            },
            "example": [
              10,
              25,
              50,
              100,
              150,
              200,
              250,
              500
            ]
          },
          "points_per_dollar": {
            "type": "integer",
            "description": "The tenant's conversion rate, for the rare screen that shows a currency figure. Everything else on this wire is points.",
            "example": 100
          }
        }
      },
      "RecognitionQuickAwardLimits": {
        "type": "object",
        "description": "What the two text fields will actually accept, read from the constants the `Award` validations are written from. Both fields are optional — the server substitutes a default when either is blank — but a value that IS sent has to clear these.",
        "required": [
          "title_min",
          "title_max",
          "message_min",
          "message_max"
        ],
        "properties": {
          "title_min": {
            "type": "integer",
            "example": 2
          },
          "title_max": {
            "type": "integer",
            "example": 100
          },
          "message_min": {
            "type": "integer",
            "description": "The one that surprises callers: a note that is sent must be at least this long, or the POST answers 422 `invalid`. Render a counter from it.",
            "example": 10
          },
          "message_max": {
            "type": "integer",
            "example": 1000
          }
        }
      },
      "RecognitionQuickAwardProgram": {
        "type": "object",
        "description": "One program in the picker, with its own categories.",
        "required": [
          "id",
          "name",
          "categories"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 7
          },
          "name": {
            "type": "string",
            "example": "Spot Awards"
          },
          "slug": {
            "type": "string",
            "example": "spot-awards"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "example": "On-the-spot recognition for going above and beyond."
          },
          "program_type": {
            "type": "string",
            "example": "manager_to_employee"
          },
          "program_type_label": {
            "type": "string",
            "nullable": true,
            "description": "The human label for `program_type`, as the web select shows it.",
            "example": "Manager to Employee"
          },
          "per_award_limit_points": {
            "type": "integer",
            "nullable": true,
            "description": "This program's own ceiling on a SINGLE award, in points. Null means no program cap — `amount.max_points` still applies. Bound the input by whichever is tighter, or the POST answers `amount_over_limit`.",
            "example": 5000
          },
          "categories": {
            "type": "array",
            "description": "Only this program's ACTIVE categories, in the picker's order. Empty when the program has none, in which case the category field has nothing to offer — a tenant that also sets `require_category` has a misconfiguration an admin has to fix.",
            "items": {
              "$ref": "#/components/schemas/RecognitionQuickAwardCategory"
            }
          }
        }
      },
      "RecognitionQuickAwardCategory": {
        "type": "object",
        "description": "A category chip and its value range, in points. `null` on a bound means unbounded on that side — which is a different answer from a bound of zero.",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 31
          },
          "name": {
            "type": "string",
            "example": "Teamwork"
          },
          "slug": {
            "type": "string",
            "example": "teamwork"
          },
          "icon": {
            "type": "string",
            "description": "Icon name, falling back to the same default the web chip draws, so an unconfigured category never renders blank.",
            "example": "handshake"
          },
          "color": {
            "type": "string",
            "description": "Hex colour, with the same web fallback.",
            "example": "#112233"
          },
          "min_points": {
            "type": "integer",
            "nullable": true,
            "description": "Lowest amount this category accepts; null when unbounded.",
            "example": 100
          },
          "max_points": {
            "type": "integer",
            "nullable": true,
            "description": "Highest amount this category accepts; null when unbounded.",
            "example": 800
          },
          "default_points": {
            "type": "integer",
            "nullable": true,
            "description": "What to re-fill the amount with when this category is picked — what the web form does. Null when the category sets no default.",
            "example": 400
          }
        }
      },
      "RecognitionQuickAwardBudget": {
        "type": "object",
        "description": "The manager's giving budget for the default program, every figure in points. `{ \"enabled\": false }` for most tenants — businesses without group budgets are unaffected by any of this — so branch on `enabled` before reading anything else.",
        "required": [
          "enabled"
        ],
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "Whether a group budget applies to this manager at all.",
            "example": true
          },
          "name": {
            "type": "string",
            "nullable": true,
            "example": "FY26 Engineering Recognition"
          },
          "status": {
            "type": "string",
            "nullable": true,
            "example": "active"
          },
          "exhausted": {
            "type": "boolean",
            "description": "The budget itself is spent out.",
            "example": false
          },
          "blocked": {
            "type": "boolean",
            "description": "The web submit button's own disabled condition — exhausted, OR this manager has nothing left. Disable Give on this rather than re-deriving it.",
            "example": false
          },
          "your_remaining_points": {
            "type": "integer",
            "description": "What THIS manager may still give from it.",
            "example": 42000
          },
          "your_spent_points": {
            "type": "integer",
            "example": 8000
          },
          "max_per_manager_points": {
            "type": "integer",
            "nullable": true,
            "description": "The per-manager cap, or null when the budget sets none.",
            "example": 50000
          },
          "available_total_points": {
            "type": "integer",
            "description": "What the whole budget has left, across every manager on it.",
            "example": 310000
          },
          "annual_allocation_points": {
            "type": "integer",
            "nullable": true,
            "example": 500000
          }
        }
      },
      "RecognitionQuickAwardRecent": {
        "type": "object",
        "description": "One row of the \"Your Recent Awards\" sidebar.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 9182
          },
          "title": {
            "type": "string",
            "example": "Shipped the migration"
          },
          "points": {
            "type": "integer",
            "description": "The award's value in points.",
            "example": 500
          },
          "awarded_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-08-17T14:05:00Z"
          },
          "recipient": {
            "$ref": "#/components/schemas/RecognitionPersonCard"
          }
        }
      },
      "RecognitionQuickAwardRequest": {
        "type": "object",
        "description": "The Quick Award form's submission. `recipient_id` and `amount` are the only required fields — the minimum the native sheet collects; everything else has a documented server-side default.",
        "required": [
          "recipient_id",
          "amount"
        ],
        "properties": {
          "recipient_id": {
            "type": "integer",
            "description": "Who is being awarded. Must be one of this caller's direct reports unless they are a Recognitions admin, who may award anyone — see `recipient_scope` on the GET, and fill the picker from `GET /recognitions/employee_suggestions`.",
            "example": 412
          },
          "amount": {
            "type": "integer",
            "description": "The award, in reward POINTS. Within `amount.min_points` .. `amount.max_points`, and also within the chosen program's `per_award_limit_points` and the chosen category's range when either is set.",
            "example": 500
          },
          "program_id": {
            "type": "integer",
            "nullable": true,
            "description": "Which program funds it. Omit for the tenant's `default_program_id`. A program that is not active in this tenant is refused with `invalid_program`, never swapped for the default.",
            "example": 7
          },
          "category_id": {
            "type": "integer",
            "nullable": true,
            "description": "A category **of the chosen program**. An id belonging to a different program is silently ignored rather than refused, matching the web form where a category can only be picked after its program. Required when `require_category` is true.",
            "example": 31
          },
          "title": {
            "type": "string",
            "nullable": true,
            "description": "A short headline. Blank becomes `\"Quick Award from {giver name}\"`; a value that is sent must fit `limits.title_min`..`title_max`.",
            "example": "Shipped the migration"
          },
          "message": {
            "type": "string",
            "nullable": true,
            "description": "The personal note. Blank becomes `\"Great work! Keep it up.\"`; a value that is sent must clear `limits.message_min` (10 characters) or the request is refused `invalid`.",
            "example": "You carried the migration all weekend — thank you."
          },
          "is_public": {
            "type": "boolean",
            "default": true,
            "description": "Whether the award appears in the recognition feed and on any connected Slack/Teams recognition channel. **Defaults to true**, because the web checkbox is pre-checked — send `false` explicitly to keep it private.",
            "example": true
          },
          "anniversary_years": {
            "type": "integer",
            "nullable": true,
            "description": "Set this ONLY when the award is the recipient's work-anniversary award — the **Recognize** action on an anniversary row. Echo that row's own `years` from `GET /recognitions/anniversary_roster` (or `upcoming_anniversaries[].years` on `GET /recognitions/team`).\n\nWith it, the award is RECORDED as that anniversary's recognition: the row comes back `recognized: true`, `status: \"recognized\"` and `can_recognize: false`, the roster's Recognized tile counts it, **Mark reward as sent** becomes available, and the automatic anniversary award is not given a second time. Without it the points still land but nothing is recorded, so the row keeps offering Recognize after every refresh.\n\nOmit it for an ordinary spot award — an inferred anniversary would cancel that employee's automatic award for the year. A value that is not one of this person's real anniversaries (the one they are heading into, or the one already passed this calendar year) is ignored; the award itself still succeeds.",
            "example": 10
          }
        }
      },
      "RecognitionQuickAwardCard": {
        "type": "object",
        "description": "The created award as the FEED renders it — the same card `GET /recognitions/feed` and `GET /recognitions/awards/{id}` return, so a client can insert it optimistically and get the same shape back on the next refresh. A Quick Award is never pending: a 201 means it is live.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "award"
            ],
            "example": "award"
          },
          "kind": {
            "type": "string",
            "enum": [
              "award"
            ],
            "description": "Always `award`, never `certificate` — a certificate is an automated lifecycle award, and a Quick Award is by definition given by a person.",
            "example": "award"
          },
          "id": {
            "type": "integer",
            "example": 9182
          },
          "title": {
            "type": "string",
            "example": "Shipped the migration"
          },
          "message": {
            "type": "string",
            "nullable": true,
            "description": "The award's description — the note the giver wrote.",
            "example": "You carried the migration all weekend — thank you."
          },
          "points": {
            "type": "integer",
            "example": 500
          },
          "recipient": {
            "$ref": "#/components/schemas/RecognitionPersonCard"
          },
          "giver": {
            "$ref": "#/components/schemas/RecognitionPersonCard"
          },
          "anonymous": {
            "type": "boolean",
            "description": "Always false — a Quick Award is attributed to its giver.",
            "example": false
          },
          "automated": {
            "type": "boolean",
            "example": false
          },
          "program": {
            "type": "string",
            "nullable": true,
            "example": "Spot Awards"
          },
          "category": {
            "type": "string",
            "nullable": true,
            "example": "Teamwork"
          },
          "visibility": {
            "type": "string",
            "enum": [
              "public",
              "private"
            ],
            "description": "Derived from `is_public`.",
            "example": "public"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "revoked",
              "expired"
            ],
            "example": "active"
          },
          "certificate_url": {
            "type": "string",
            "nullable": true,
            "description": "Absolute URL of the printable gold-framed certificate page for this award.",
            "example": "https://acme.workforce.mangoapps.com/recognition/awards/9182/card"
          },
          "tags": {
            "type": "array",
            "description": "Always empty — tags are a peer-recognition field, not an award one.",
            "items": {
              "type": "string"
            },
            "example": []
          },
          "boost": {
            "nullable": true,
            "description": "Always null — awards cannot be boosted; only peer gives can."
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-08-17T14:05:00Z"
          }
        }
      },
      "RecognitionQuickAwardRefusal": {
        "type": "object",
        "description": "A 403 from either Quick Award verb.",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "access_denied",
                  "feature_disabled",
                  "forbidden"
                ],
                "description": "`access_denied` — no access to the Recognitions app. `feature_disabled` — the tenant switched Quick Awards off; hide the affordance. `forbidden` — the caller is not a reviewer."
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "RecognitionQuickAwardError": {
        "type": "object",
        "description": "A refused give. Nothing was written. `code` names the rule so a client can highlight the offending field; `message` is the wording the web flash shows and is safe to display as-is.",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "invalid_recipient",
                  "recipient_not_permitted",
                  "invalid_amount",
                  "amount_over_limit",
                  "category_required",
                  "governance_blocked",
                  "budget_exceeded",
                  "invalid_program",
                  "no_program",
                  "invalid",
                  "internal_error"
                ],
                "example": "amount_over_limit"
              },
              "message": {
                "type": "string",
                "example": "Award amount cannot exceed $100.00."
              },
              "details": {
                "type": "object",
                "nullable": true,
                "description": "Present on the amount refusals only — the bounds the amount was measured against, so a client can correct the field rather than asking the user to guess.",
                "properties": {
                  "min_points": {
                    "type": "integer",
                    "example": 1
                  },
                  "max_points": {
                    "type": "integer",
                    "example": 10000
                  }
                }
              }
            }
          }
        }
      },
      "RecognitionEmployeeSuggestion": {
        "type": "object",
        "description": "One suggestible recipient. The shared Recognitions person card plus the two fields a recipient picker needs, and which the web picker already shows: `email` (the disambiguator — \"Chris Taylor\" is two people in most tenants, and a picker that can't tell them apart sends recognition to the wrong one) and `department` (the row's secondary line).",
        "properties": {
          "id": {
            "type": "integer",
            "example": 412
          },
          "name": {
            "type": "string",
            "example": "Maya Chen"
          },
          "title": {
            "type": "string",
            "nullable": true,
            "description": "Job title, or null when unset.",
            "example": "Support Specialist"
          },
          "image": {
            "type": "string",
            "nullable": true,
            "description": "Absolute avatar URL, or null when it can't be resolved."
          },
          "email": {
            "type": "string",
            "nullable": true,
            "example": "maya.chen@example.com"
          },
          "department": {
            "type": "string",
            "nullable": true,
            "description": "Primary organizational department name, or null.",
            "example": "Customer Support"
          }
        }
      },
      "RecognitionEmployeeSuggestionsMeta": {
        "type": "object",
        "description": "The page envelope every paginated Recognitions endpoint answers in, plus the three fields a picker needs to render the right empty state and the right scope copy.",
        "properties": {
          "current_page": {
            "type": "integer",
            "example": 1
          },
          "per_page": {
            "type": "integer",
            "example": 25
          },
          "total_count": {
            "type": "integer",
            "description": "Total suggestible colleagues matching this search and scope.",
            "example": 148
          },
          "total_pages": {
            "type": "integer",
            "example": 6
          },
          "has_next_page": {
            "type": "boolean"
          },
          "has_prev_page": {
            "type": "boolean"
          },
          "query": {
            "type": "string",
            "nullable": true,
            "description": "The search term as applied (trimmed; null when blank). Echoed so a client can tell \"nobody matches Xyz\" from \"you have no colleagues to recognize\" and render the right empty state for each.",
            "example": "maya"
          },
          "scope": {
            "type": "string",
            "enum": [
              "all",
              "direct_reports"
            ],
            "description": "The scope as applied — `all` when an unrecognized value was sent."
          },
          "direct_reports_only": {
            "type": "boolean",
            "description": "True when the roster was actually narrowed to this caller's direct reports, i.e. `scope=direct_reports` AND the caller is not an admin (an admin may Quick Award anyone, so nothing is narrowed for them). Read THIS, not the `scope` you sent, before showing \"your team only\" copy."
          }
        }
      },
      "RecognitionPersonCard": {
        "type": "object",
        "description": "The shared person card used across the Recognitions API.",
        "properties": {
          "id": {
            "type": "integer",
            "nullable": true,
            "example": 42
          },
          "name": {
            "type": "string",
            "example": "Maya Chen"
          },
          "title": {
            "type": "string",
            "nullable": true,
            "example": "Staff Engineer"
          },
          "image": {
            "type": "string",
            "nullable": true,
            "description": "Absolute avatar URL."
          }
        }
      },
      "RecognitionEngagement": {
        "type": "object",
        "description": "Reaction and comment engagement, including the full list of reactions.",
        "required": [
          "reactions_count",
          "comments_count",
          "reactions"
        ],
        "properties": {
          "reactions_count": {
            "type": "integer",
            "example": 3
          },
          "reactions_enabled": {
            "type": "boolean",
            "example": true
          },
          "my_reaction": {
            "type": "string",
            "nullable": true,
            "description": "The viewer's own reaction emoji, if any.",
            "example": "🎉"
          },
          "reaction_summary": {
            "type": "array",
            "description": "Reactions grouped by emoji, most-used first.",
            "items": {
              "type": "object",
              "properties": {
                "emoji": {
                  "type": "string",
                  "example": "👍"
                },
                "count": {
                  "type": "integer",
                  "example": 2
                }
              }
            }
          },
          "reactions": {
            "type": "array",
            "description": "Every reaction on this recognition, oldest first — emoji plus who left it.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 55012
                },
                "emoji": {
                  "type": "string",
                  "example": "👍"
                },
                "label": {
                  "type": "string",
                  "example": "Like"
                },
                "user": {
                  "$ref": "#/components/schemas/RecognitionPersonCard"
                },
                "reacted_at": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            }
          },
          "comments_count": {
            "type": "integer",
            "example": 2
          },
          "comments_enabled": {
            "type": "boolean",
            "example": true
          },
          "shares_count": {
            "type": "integer",
            "example": 0,
            "description": "Always 0 for awards."
          }
        }
      },
      "RecognitionBoost": {
        "type": "object",
        "description": "Peer pile-on points (posts only).",
        "properties": {
          "total_points": {
            "type": "integer",
            "example": 25,
            "description": "Points added by boosters so far."
          },
          "boosted_by_me": {
            "type": "boolean",
            "example": false
          },
          "can_boost": {
            "type": "boolean",
            "example": true
          },
          "amounts": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "description": "The boost amounts the viewer may give, capped to their remaining allowance.",
            "example": [
              5,
              10,
              25
            ]
          }
        }
      },
      "RecognitionBoostReceipt": {
        "type": "object",
        "description": "The boost that was just applied, returned by `POST /recognitions/posts/{id}/boost`. The points have already left the booster's allowance and landed in the recipient's store balance — this is a receipt, not a pending request.",
        "required": [
          "id",
          "points",
          "booster",
          "recipient"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 812
          },
          "points": {
            "type": "integer",
            "enum": [
              5,
              10,
              25
            ],
            "example": 10
          },
          "boosted_at": {
            "type": "string",
            "format": "date-time"
          },
          "booster": {
            "allOf": [
              {
                "$ref": "#/components/schemas/RecognitionPersonCard"
              }
            ],
            "description": "The caller — the person whose allowance paid for this boost."
          },
          "recipient": {
            "allOf": [
              {
                "$ref": "#/components/schemas/RecognitionPersonCard"
              }
            ],
            "description": "The recognition's recipient — the person whose balance was credited."
          }
        }
      },
      "RecognitionBoostTarget": {
        "type": "object",
        "description": "The boosted post's refreshed state, so a client can patch the card it already has rather than re-fetching the feed or the detail screen. The `boost` block is the SAME shape those two endpoints return.",
        "required": [
          "id",
          "type",
          "boost"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 4471
          },
          "type": {
            "type": "string",
            "enum": [
              "recognition_post"
            ],
            "description": "Always `recognition_post` — awards are not boostable."
          },
          "boost": {
            "allOf": [
              {
                "$ref": "#/components/schemas/RecognitionBoost"
              }
            ],
            "description": "Recomputed after the write, so `boosted_by_me` is true, `can_boost` is false (one boost per person per post) and `amounts` is empty."
          }
        }
      },
      "RecognitionBoostError": {
        "type": "object",
        "description": "A refused boost. `details` carries the post's CURRENT boost block and the caller's remaining allowance, so a client whose card was stale can reconcile from the refusal instead of re-fetching the screen.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "invalid_boost",
                  "insufficient_allowance",
                  "boost_failed"
                ],
                "example": "insufficient_allowance"
              },
              "message": {
                "type": "string",
                "description": "Safe to show to the user verbatim — the same wording the web flashes.",
                "example": "You only have 5 giving points left this month."
              },
              "details": {
                "type": "object",
                "properties": {
                  "boost": {
                    "$ref": "#/components/schemas/RecognitionBoost"
                  },
                  "giving_remaining": {
                    "type": "integer",
                    "description": "The caller's remaining monthly giving allowance (unchanged — a refusal spends nothing).",
                    "example": 5
                  }
                }
              }
            }
          }
        }
      },
      "RecognitionPermissions": {
        "type": "object",
        "description": "Per-viewer capability flags — the affordances the server would honour.",
        "required": [
          "can_edit",
          "can_delete",
          "can_boost"
        ],
        "properties": {
          "can_edit": {
            "type": "boolean",
            "description": "May PATCH this recognition's message/company value: the author or a moderator on a post, a moderator on an award, and only while it is still active."
          },
          "can_delete": {
            "type": "boolean",
            "description": "Author/admin delete for a post; admin revoke for an award."
          },
          "can_boost": {
            "type": "boolean",
            "description": "Peer pile-on eligibility; always false for awards."
          },
          "can_share": {
            "type": "boolean"
          },
          "can_comment": {
            "type": "boolean"
          },
          "can_react": {
            "type": "boolean"
          }
        }
      },
      "WrappedPersonCount": {
        "type": "object",
        "description": "A person named in a Recognition Wrapped list, plus how many recognitions were exchanged with them.",
        "required": [
          "user",
          "count"
        ],
        "properties": {
          "user": {
            "type": "object",
            "description": "The shared person card used across the Recognitions API.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 42
              },
              "name": {
                "type": "string",
                "example": "Maya Chen"
              },
              "title": {
                "type": "string",
                "nullable": true,
                "example": "Engineering Manager"
              },
              "image": {
                "type": "string",
                "nullable": true,
                "description": "Absolute avatar URL",
                "or null.": null,
                "example": "https://cdn.example.com/avatars/42.jpg"
              }
            }
          },
          "count": {
            "type": "integer",
            "description": "Number of recognitions exchanged with this person this year.",
            "example": 4
          }
        }
      },
      "RecognitionBulkReviewResult": {
        "type": "object",
        "description": "The outcome of a bulk approve/reject. A 200 means the batch was accepted; it may still carry skipped or errored rows, so read the counts. The three lists partition the requested ids: `processed` succeeded, `skipped_ids` were not acted on (not reviewable / already decided / another tenant's), and `errors` failed mid-write.",
        "required": [
          "action",
          "requested_count",
          "processed_count",
          "skipped_count",
          "error_count",
          "processed",
          "skipped_ids",
          "errors"
        ],
        "properties": {
          "action": {
            "type": "string",
            "enum": [
              "approve",
              "reject"
            ],
            "example": "approve"
          },
          "message": {
            "type": "string",
            "description": "A human-readable summary, mirroring the web flash.",
            "example": "3 nominations approved."
          },
          "requested_count": {
            "type": "integer",
            "description": "How many distinct ids the caller submitted (after de-duplication).",
            "example": 5
          },
          "processed_count": {
            "type": "integer",
            "example": 3
          },
          "skipped_count": {
            "type": "integer",
            "example": 2
          },
          "error_count": {
            "type": "integer",
            "example": 0
          },
          "processed": {
            "type": "array",
            "description": "The nominations that were approved/rejected.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 8123
                },
                "nominee": {
                  "$ref": "#/components/schemas/RecognitionPerson"
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "pending",
                    "under_review",
                    "approved",
                    "rejected",
                    "cancelled"
                  ],
                  "example": "approved"
                },
                "finalized": {
                  "type": "boolean",
                  "description": "True once terminal (award minted / rejected); false while a multi-level chain is still advancing.",
                  "example": true
                },
                "approval_level": {
                  "type": "integer",
                  "description": "Which approval step is current, of how many.",
                  "example": 1
                },
                "approval_levels_required": {
                  "type": "integer",
                  "example": 1
                }
              }
            }
          },
          "skipped_ids": {
            "type": "array",
            "description": "Requested ids that were NOT acted on — not reviewable by the caller, already decided, or another tenant's.",
            "items": {
              "type": "integer"
            },
            "example": [
              8140,
              8141
            ]
          },
          "errors": {
            "type": "array",
            "description": "Per-row failures. Each names the nominee and the reason the write raised.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 8130
                },
                "name": {
                  "type": "string",
                  "example": "Ada Lovelace"
                },
                "message": {
                  "type": "string",
                  "example": "Validation failed: Approved value must be greater than or equal to 0"
                }
              }
            }
          },
          "unread_notification_count": {
            "type": "integer",
            "description": "The caller's unread notification count, for the app badge.",
            "example": 3
          }
        }
      },
      "RecognitionNominationInput": {
        "type": "object",
        "description": "The nominate composer's fields — the same set the web form (`recognition/nominate.html.erb`) submits. Build the pickers from `GET /recognitions/config`.",
        "required": [
          "recognition_program_id",
          "nominee_id",
          "title",
          "description"
        ],
        "properties": {
          "recognition_program_id": {
            "type": "integer",
            "description": "The award being nominated for. Must be a program that is nominatable right now AND open to this caller — `config.programs[]` reports both as `can_nominate`.",
            "example": 41
          },
          "nominee_id": {
            "type": "integer",
            "description": "The colleague being nominated. Must be a member of the caller's business, and cannot be the caller. Picker: `GET /recognitions/employee_suggestions`.",
            "example": 9021
          },
          "title": {
            "type": "string",
            "description": "A short award-worthy headline. Bounds are `config.limits.nomination_title_*`.",
            "minLength": 2,
            "maxLength": 100,
            "example": "Renewal save of the quarter"
          },
          "description": {
            "type": "string",
            "description": "What the nominee did that deserves recognition. Bounds are `config.limits.nomination_description_*`.",
            "minLength": 10,
            "maxLength": 1000,
            "example": "Turned a churn risk into a multi-year renewal."
          },
          "recognition_category_id": {
            "type": "integer",
            "nullable": true,
            "description": "Optional. Must belong to the chosen program (`config.programs[].categories`). Its `min_points` / `max_points` bound `requested_value`.",
            "example": 77
          },
          "justification": {
            "type": "string",
            "nullable": true,
            "description": "Optional supporting detail — the measurable impact. Cap is `config.limits.nomination_justification_max`.",
            "maxLength": 2000,
            "example": "Cut renewal turnaround from 5 days to 1 across 40 accounts."
          },
          "requested_value": {
            "type": "integer",
            "nullable": true,
            "description": "Optional suggested award value in reward POINTS (converted server-side to stored dollars at `config.economy.points_per_dollar`). Must be zero or greater, and within the chosen category's range and the program's per-award cap.",
            "example": 250
          },
          "is_public": {
            "type": "boolean",
            "nullable": true,
            "description": "Whether the nomination appears in the recognition feed. OMITTED falls back to the TENANT's configured default visibility, not to `true`.",
            "example": true
          },
          "is_anonymous": {
            "type": "boolean",
            "nullable": true,
            "description": "Hide the nominator's name. Silently forced to `false` while the tenant has anonymous recognition switched off (`config.permissions` / `visibility_options` report whether it is offered), exactly as the web form hides the checkbox.",
            "example": false
          }
        }
      },
      "RecognitionNominationDecision": {
        "type": "object",
        "description": "One nomination row — the shared card that both reviewer decisions (approve/reject) and the file endpoint (`POST /recognitions/nominations`) answer with, so a client keys off ONE shape across the whole nomination lifecycle. `reviewer_notes` carries the decision note (the approval note after approve, the rejection reason after reject) and is null on a freshly filed nomination; `approval_complete` is true once the request reaches a terminal state (award minted, or rejected) and false while a multi-level chain is still advancing — a nomination filed into an approval-off tenant is born complete.",
        "properties": {
          "type": {
            "type": "string",
            "description": "Always `nomination`, so a client can key off one row shape.",
            "example": "nomination"
          },
          "id": {
            "type": "integer",
            "example": 8123
          },
          "title": {
            "type": "string",
            "example": "Renewal save of the quarter"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "under_review",
              "approved",
              "rejected",
              "cancelled"
            ],
            "example": "approved"
          },
          "display_status": {
            "type": "string",
            "example": "Approved"
          },
          "status_color": {
            "type": "string",
            "description": "The hex the web badge is painted with, so both surfaces agree on the status colour.",
            "example": "#16a34a"
          },
          "approval_complete": {
            "type": "boolean",
            "description": "True once the request is terminal (award minted / rejected); false while advancing a multi-level chain.",
            "example": true
          },
          "nominee": {
            "$ref": "#/components/schemas/RecognitionPerson"
          },
          "nominator": {
            "$ref": "#/components/schemas/RecognitionPerson"
          },
          "reviewer": {
            "allOf": [
              {
                "$ref": "#/components/schemas/RecognitionPerson"
              }
            ],
            "nullable": true,
            "description": "The deciding reviewer, or the NEXT approver on a multi-level advance."
          },
          "program": {
            "type": "string",
            "nullable": true,
            "example": "Spot Awards"
          },
          "category": {
            "type": "string",
            "nullable": true,
            "example": "Customer Impact"
          },
          "approval_level": {
            "type": "integer",
            "description": "Which approval step is current, of how many.",
            "example": 1
          },
          "approval_levels_required": {
            "type": "integer",
            "example": 1
          },
          "points": {
            "type": "integer",
            "description": "What the nomination is worth right now, in reward POINTS — the approved figure once a reviewer has set one, the requested figure until then. The same key (and unit) the My Recognition rows carry, so one card component renders a nomination wherever it meets one. 0 when it was filed without a value.",
            "example": 250
          },
          "requested_value": {
            "type": "string",
            "nullable": true,
            "description": "The originally requested award value, in stored DOLLARS, as a decimal string. Use `requested_points` unless you specifically mean money.",
            "example": "2.5"
          },
          "approved_value": {
            "type": "string",
            "nullable": true,
            "description": "The approved award value, in stored dollars (points ÷ points-per-dollar), as a decimal string. Use `approved_points` unless you specifically mean money.",
            "example": "5.0"
          },
          "requested_points": {
            "type": "integer",
            "nullable": true,
            "description": "`requested_value` in reward points. Null (not 0) when no value was asked for.",
            "example": 250
          },
          "approved_points": {
            "type": "integer",
            "nullable": true,
            "description": "`approved_value` in reward points. Null until the nomination is approved.",
            "example": 500
          },
          "reviewer_notes": {
            "type": "string",
            "nullable": true,
            "description": "The decision note — the approval note, or the rejection reason.",
            "example": "Clear, measurable impact on the renewal — well earned."
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "reviewed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "RecognitionAwardCycleBase": {
        "type": "object",
        "description": "The fields EVERY award-cycle card carries, whichever list it is in, so a client reads one shape and the four lists stay comparable.",
        "required": [
          "id",
          "name",
          "status",
          "status_label",
          "selection_mode",
          "opens_at",
          "closes_at",
          "award_points",
          "nomination_count",
          "nominee_count"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 6
          },
          "name": {
            "type": "string",
            "example": "Mango Champions — August"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "example": "Our monthly peer-nominated award for people who make the work better for everyone."
          },
          "criteria": {
            "type": "string",
            "nullable": true,
            "description": "\"Looking for:\" on the web card — what a good nomination looks like. Show it in the nominate form; it is the whole point of a criteria-led award.",
            "example": "Someone who went out of their way to help a teammate or a customer this month."
          },
          "status": {
            "type": "string",
            "enum": [
              "scheduled",
              "open",
              "closed",
              "decided"
            ],
            "description": "The EFFECTIVE status, which reconciles the stored column against the live window — not the stored `status`. This is what decides the list the card is in. Never re-derive it from the timestamps.",
            "example": "open"
          },
          "status_label": {
            "type": "string",
            "description": "The pill the web card paints next to the title.",
            "enum": [
              "Opens soon",
              "Open",
              "Awaiting decision",
              "Winners announced"
            ],
            "example": "Open"
          },
          "status_badge_class": {
            "type": "string",
            "description": "The Bootstrap class the web pill uses, so a native pill can carry the same treatment without maintaining its own mapping.",
            "example": "bg-success-subtle text-success-emphasis"
          },
          "selection_mode": {
            "type": "string",
            "enum": [
              "spotlight",
              "committee"
            ],
            "description": "`spotlight` recognizes everyone meeting the distinct-nominator threshold; `committee` is decided by a named committee after the window closes.",
            "example": "spotlight"
          },
          "selection_mode_label": {
            "type": "string",
            "enum": [
              "Spotlight",
              "Committee"
            ],
            "example": "Spotlight"
          },
          "opens_at": {
            "type": "string",
            "format": "date-time"
          },
          "closes_at": {
            "type": "string",
            "format": "date-time"
          },
          "recurrence": {
            "type": "string",
            "enum": [
              "none",
              "weekly",
              "monthly",
              "quarterly"
            ],
            "example": "monthly"
          },
          "recurring": {
            "type": "boolean",
            "description": "Whether the next cycle is created automatically when this one is decided.",
            "example": true
          },
          "award_points": {
            "type": "integer",
            "description": "The prize, in the reward POINTS employees see everywhere else in this app. 0 means recognition only — no points attached.",
            "example": 500
          },
          "program": {
            "type": "object",
            "nullable": true,
            "description": "The recognition program this cycle runs under.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 1
              },
              "name": {
                "type": "string",
                "example": "Spot Recognition"
              }
            }
          },
          "category": {
            "type": "object",
            "nullable": true,
            "description": "Null when the cycle isn't scoped to one category.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 1
              },
              "name": {
                "type": "string",
                "example": "Teamwork"
              }
            }
          },
          "nomination_count": {
            "type": "integer",
            "description": "Total nominations pooled in this cycle.",
            "example": 7
          },
          "nominee_count": {
            "type": "integer",
            "description": "DISTINCT people nominated — the size of the pool a committee actually decides between. Never greater than `nomination_count`; 7 nominations from 3 nominees is a different job than 7 from 7.",
            "example": 3
          },
          "icon": {
            "type": "string",
            "description": "Cycles carry no icon of their own; every surface uses the trophy. Same glyph the dashboard's running-cycle banner sends.",
            "example": "fas fa-trophy"
          }
        }
      },
      "RecognitionAwardCycleOpenCard": {
        "allOf": [
          {
            "$ref": "#/components/schemas/RecognitionAwardCycleBase"
          },
          {
            "type": "object",
            "description": "An open cycle — the hero card, and the only list with a CTA.",
            "required": [
              "days_left",
              "closing_soon",
              "can_nominate",
              "nominated_by_me",
              "my_nomination_count"
            ],
            "properties": {
              "days_left": {
                "type": "integer",
                "description": "Whole days until nominations close, floor 0 — the same arithmetic the dashboard banner uses, so the two screens never differ by a day for the same cycle.",
                "example": 8
              },
              "closing_soon": {
                "type": "boolean",
                "description": "True when `days_left` is within `meta.closing_soon_days`.",
                "example": false
              },
              "threshold_nominators": {
                "type": "integer",
                "nullable": true,
                "description": "How many DISTINCT nominators a nominee needs before a Spotlight cycle recognizes them. **Null for a committee cycle** — the committee decides, so a threshold is not what a nominator should aim at.",
                "example": 3
              },
              "can_nominate": {
                "type": "boolean",
                "description": "Whether this caller may nominate RIGHT NOW — the same answer the submit path gives. Hide the Nominate affordance when false.",
                "example": true
              },
              "nomination_block_reason": {
                "type": "string",
                "nullable": true,
                "description": "Why not, in the caller's words. Null when they can. Show this instead of a bare disabled button.",
                "example": "Peer recognition is not enabled"
              },
              "nominated_by_me": {
                "type": "boolean",
                "description": "Whether this caller has already nominated into this cycle. STATE, not a gate — a cycle pools nominations, so nominating again is allowed. Use it to say \"you've nominated\" rather than re-offering a fresh CTA with no memory.",
                "example": false
              },
              "my_nomination_count": {
                "type": "integer",
                "description": "How many nominations this caller filed here. Never exceeds `nomination_count`.",
                "example": 0
              },
              "nominate_url": {
                "type": "string",
                "description": "Absolute URL of the WEB nominate form (a native client can't resolve a relative path). Still sent because a WebView client uses it; a native client wants the two below.",
                "example": "https://acme.workforce.mangoapps.com/recognition/award-cycles/6/nominate"
              },
              "nominate_form_api_url": {
                "type": "string",
                "description": "Absolute URL of the NATIVE composer — `GET /recognitions/award_cycles/{id}/nominations/new`. Present even when `can_nominate` is false: the two answer different questions, so hide the CTA on `can_nominate` (showing `nomination_block_reason`), never on the absence of a URL.",
                "example": "https://acme.workforce.mangoapps.com/api/v1/recognitions/award_cycles/6/nominations/new"
              },
              "nominate_api_url": {
                "type": "string",
                "description": "Absolute URL to POST the nomination to — `POST /recognitions/award_cycles/{id}/nominations`. Sent so a client that got this card from the browse list never rebuilds the path.",
                "example": "https://acme.workforce.mangoapps.com/api/v1/recognitions/award_cycles/6/nominations"
              }
            }
          }
        ]
      },
      "RecognitionAwardCycleUpcomingCard": {
        "allOf": [
          {
            "$ref": "#/components/schemas/RecognitionAwardCycleBase"
          },
          {
            "type": "object",
            "description": "A scheduled cycle. Deliberately thinner than an open card — there is no CTA, because the window hasn't opened and the submit path would refuse.",
            "required": [
              "opens_in_days",
              "can_nominate"
            ],
            "properties": {
              "opens_in_days": {
                "type": "integer",
                "description": "Whole days until the window opens, floor 0.",
                "example": 12
              },
              "can_nominate": {
                "type": "boolean",
                "description": "Always false in this list.",
                "example": false
              },
              "nomination_block_reason": {
                "type": "string",
                "example": "This award isn't open for nominations right now."
              }
            }
          }
        ]
      },
      "RecognitionAwardCycleReviewCard": {
        "allOf": [
          {
            "$ref": "#/components/schemas/RecognitionAwardCycleBase"
          },
          {
            "type": "object",
            "description": "A closed committee cycle THIS caller is asked to decide. Everything in this list already passed the membership + recusal test.",
            "required": [
              "can_review",
              "my_pick_count"
            ],
            "properties": {
              "can_review": {
                "type": "boolean",
                "description": "The wider controller gate, which ALSO admits an admin who isn't on the committee. True by construction for anything in `needs_my_review`; present so a client reads the same key whether it got the cycle from this list or built the link itself.",
                "example": true
              },
              "my_pick_count": {
                "type": "integer",
                "description": "How many nominees this member has already recommended — so the prompt can distinguish \"you haven't started\" from \"you've submitted picks\".",
                "example": 1
              },
              "review_url": {
                "type": "string",
                "description": "Absolute URL of the anonymized committee review page.",
                "example": "https://acme.workforce.mangoapps.com/recognition/award-cycles/10/review"
              }
            }
          }
        ]
      },
      "RecognitionAwardCycleDecidedCard": {
        "allOf": [
          {
            "$ref": "#/components/schemas/RecognitionAwardCycleBase"
          },
          {
            "type": "object",
            "description": "A decided cycle — the \"Recent winners\" row, expanded with everything the web results page shows, so a client can render the row AND its results screen from this one response.",
            "required": [
              "decided_at",
              "winners",
              "winner_count",
              "winner_names",
              "honor_roll",
              "honor_roll_count"
            ],
            "properties": {
              "decided_at": {
                "type": "string",
                "format": "date-time",
                "description": "When the winners were announced — the date under the trophy hero."
              },
              "announcement_published": {
                "type": "boolean",
                "description": "Whether the company-wide winner announcement was actually published (a decide in \"draft\" mode leaves it for an admin), so a client doesn't imply a broadcast that never went out.",
                "example": true
              },
              "winners": {
                "type": "array",
                "description": "Only the people who actually won. Empty when nobody met the bar — a real outcome for a Spotlight cycle whose threshold nobody reached.",
                "items": {
                  "$ref": "#/components/schemas/RecognitionAwardCycleWinner"
                }
              },
              "winner_count": {
                "type": "integer",
                "example": 2
              },
              "winner_names": {
                "type": "array",
                "description": "The names in display order, for a client that renders the web row's one-line sentence (\"Alice and Bob\"). Always parallel to `winners`.",
                "items": {
                  "type": "string"
                },
                "example": [
                  "J.Quack Kolb",
                  "Weston D'Amore"
                ]
              },
              "honor_roll": {
                "type": "array",
                "description": "EVERYONE who was nominated, winners included — \"being nominated by a colleague is itself recognition\". Never larger than `nominee_count`.",
                "items": {
                  "$ref": "#/components/schemas/RecognitionPerson"
                }
              },
              "honor_roll_count": {
                "type": "integer",
                "example": 3
              },
              "results_url": {
                "type": "string",
                "description": "Absolute URL of the public results / honor-roll page.",
                "example": "https://acme.workforce.mangoapps.com/recognition/award-cycles/11/results"
              }
            }
          }
        ]
      },
      "RecognitionAwardCycleWinner": {
        "type": "object",
        "description": "One winner of a decided cycle, with the certificate to show them.",
        "required": [
          "user",
          "label",
          "title"
        ],
        "properties": {
          "user": {
            "$ref": "#/components/schemas/RecognitionPerson"
          },
          "label": {
            "type": "string",
            "description": "The caption under the name on the web winner card.",
            "example": "Winner"
          },
          "title": {
            "type": "string",
            "description": "The award's own title, falling back to \"<cycle> — Winner\" when no Award record exists.",
            "example": "Mango Champions — July — Winner"
          },
          "certificate": {
            "allOf": [
              {
                "$ref": "#/components/schemas/RecognitionAwardCycleCertificate"
              }
            ],
            "nullable": true,
            "description": "**Nullable.** The decide step logs and continues when an Award can't be minted, so a winner with no certificate is a real state. Handle the null rather than assuming one is always present."
          }
        }
      },
      "RecognitionAwardCycleCertificate": {
        "type": "object",
        "description": "The printable certificate's fields, in the order the printed page renders them: title, the recipient, who awarded it, the italic citation quote, the program / points / company-value chips, then the footer's issuing organization and date.\n\n**One block, two endpoints.** This is exactly what `GET /recognitions/awards/{id}/certificate` returns standalone, and what a decided cycle's `winners[].certificate` embeds. Both are produced by one serializer, so a client can render the \"View Certificate\" overlay from either source and can never get two different certificates for one award.",
        "required": [
          "award_id",
          "title",
          "recipient",
          "points",
          "chips",
          "unit",
          "certificate_url",
          "share_url"
        ],
        "properties": {
          "award_id": {
            "type": "integer",
            "example": 23
          },
          "title": {
            "type": "string",
            "example": "Mango Champions — July — Winner"
          },
          "recipient": {
            "allOf": [
              {
                "$ref": "#/components/schemas/RecognitionPerson"
              }
            ],
            "nullable": true,
            "description": "The person the certificate is presented to. Carried on the block itself — not only on the enclosing winner card — so a client holding just this certificate (from a QR scan or a shared link) can render the whole thing from it."
          },
          "citation": {
            "type": "string",
            "nullable": true,
            "description": "The italic quote on the certificate — the reason this person won.",
            "example": "Recognized through Mango Champions — July."
          },
          "awarded_by": {
            "allOf": [
              {
                "$ref": "#/components/schemas/RecognitionPerson"
              }
            ],
            "nullable": true,
            "description": "**Null when the award was anonymous** — naming the giver is exactly the disclosure they opted out of. Automated awards present as a \"System (Automated)\" principal with a null id rather than leaking the system account."
          },
          "program": {
            "type": "string",
            "nullable": true,
            "example": "Spot Recognition"
          },
          "category": {
            "type": "string",
            "nullable": true,
            "example": "Teamwork"
          },
          "company_value": {
            "type": "string",
            "nullable": true,
            "example": "Teamwork First"
          },
          "points": {
            "type": "integer",
            "description": "The reward points on the certificate chip. The economy is STORED in dollars and PRESENTED in points (1 point = 1¢); this is the already-converted figure.",
            "example": 500
          },
          "points_label": {
            "type": "string",
            "nullable": true,
            "description": "The points chip's label, delimited exactly as the printed certificate renders it. **Null when the award carries no points** — the web view prints the coins badge only for a positive value, so a recognition-only award must not show a \"0 pts\" chip.",
            "example": "500 pts"
          },
          "chips": {
            "type": "array",
            "description": "The certificate's badges, pre-ordered and pre-glyphed as the web page paints them (program → points → company value), so the client keeps no mapping of its own. Each appears exactly when its source field does; the array is empty for an award with no program, points or value tag.",
            "items": {
              "type": "object",
              "required": [
                "key",
                "label",
                "icon"
              ],
              "properties": {
                "key": {
                  "type": "string",
                  "enum": [
                    "program",
                    "points",
                    "company_value"
                  ],
                  "example": "program"
                },
                "label": {
                  "type": "string",
                  "example": "Mango Champions"
                },
                "icon": {
                  "type": "string",
                  "description": "Font Awesome class, the same glyph the web badge uses.",
                  "example": "fas fa-trophy"
                }
              }
            }
          },
          "unit": {
            "type": "string",
            "description": "The issuing organization, as the certificate footer prints it (\"unit · date\").",
            "example": "OfficeChat"
          },
          "awarded_at": {
            "type": "string",
            "format": "date-time"
          },
          "awarded_on": {
            "type": "string",
            "description": "The award date, pre-formatted as the printed certificate shows it.",
            "example": "August 13, 2026"
          },
          "certificate_url": {
            "type": "string",
            "description": "Absolute URL of the **printable** certificate — the print-layout page behind Print / Save. (No QR is rendered on it: a scan-to-view square reads fine on digital signage but out of place on a formal, often printed, certificate.)",
            "example": "https://acme.workforce.mangoapps.com/recognition/awards/23/card"
          },
          "share_url": {
            "type": "string",
            "description": "Absolute URL of the award **permalink** — the in-app feed presentation. This is what a Share action sends and what the certificate's scan-to-view QR should encode. Deliberately a different page from `certificate_url`, which would dead-end a phone that scanned it in a print dialog.",
            "example": "https://acme.workforce.mangoapps.com/recognition/awards/23"
          }
        }
      },
      "RecognitionCertificateMeta": {
        "type": "object",
        "description": "The printed certificate's fixed copy, sent so the native overlay says what the printed page says instead of hardcoding English strings in the client. Only `GET /recognitions/awards/{id}/certificate` carries it.\n\n**Copy only — no URLs.** The two the screen needs (`certificate_url` for Print/Save, `share_url` for the QR and Share) live on the certificate itself, where they belong to the award.",
        "required": [
          "eyebrow",
          "presented_label",
          "awarded_by_prefix",
          "scan_label"
        ],
        "properties": {
          "eyebrow": {
            "type": "string",
            "description": "The gold uppercase line above the award title.",
            "example": "Certificate of Recognition"
          },
          "presented_label": {
            "type": "string",
            "description": "The line between the award title and the recipient's name.",
            "example": "is proudly presented to"
          },
          "awarded_by_prefix": {
            "type": "string",
            "description": "Prefix for the giver line, rendered as \"awarded by **{name}**\". Omit the whole line when `awarded_by` is null.",
            "example": "awarded by"
          },
          "scan_label": {
            "type": "string",
            "description": "The caption under the scan-to-view QR square.",
            "example": "Scan to view"
          }
        }
      },
      "RecognitionAwardCycleResultsMeta": {
        "type": "object",
        "description": "The Award Results page's own copy and states, sent so the native screen says what the web screen says instead of inventing its own wording. Only `GET /recognitions/award_cycles/{id}/results` carries it — the list's `recent_winners` cards do not.",
        "required": [
          "honor_roll_title",
          "honor_roll_note",
          "capabilities"
        ],
        "properties": {
          "announced_label": {
            "type": "string",
            "description": "The web page's subtitle under the cycle name, pre-formatted as it prints it. **Absent** when a decided cycle carries no `decided_at`, in which case the web page omits the line entirely — omit it too rather than printing \"announced \".",
            "example": "Winners announced July 13, 2026"
          },
          "honor_roll_title": {
            "type": "string",
            "description": "The heading over the honor-roll card.",
            "example": "Honor roll — everyone who was nominated"
          },
          "honor_roll_note": {
            "type": "string",
            "description": "The thank-you line inside the honor-roll card.",
            "example": "Being nominated by a colleague is itself recognition. Thank you, all of you."
          },
          "empty_state": {
            "type": "object",
            "description": "Present **only** when the cycle produced no winners — a real outcome for a Spotlight cycle nobody cleared the threshold in, and the state this copy describes. The honor roll can still be non-empty. Carries no `message`: the web state is a single line.",
            "required": [
              "key",
              "title"
            ],
            "properties": {
              "key": {
                "type": "string",
                "example": "winners"
              },
              "title": {
                "type": "string",
                "example": "No winners met the bar this cycle."
              }
            }
          },
          "capabilities": {
            "type": "object",
            "description": "The tenant toggles, so one payload shape describes both nomination models. `award_cycles_enabled` is always true in a 200 — the endpoint 403s otherwise.",
            "properties": {
              "award_cycles_enabled": {
                "type": "boolean",
                "example": true
              },
              "award_requests_enabled": {
                "type": "boolean",
                "example": true
              }
            }
          }
        }
      },
      "RecognitionProgramCard": {
        "type": "object",
        "description": "One recognition program as the in-app card renders it: what it is, the viewer's own standing in it, its remaining points, its categories, and whether the viewer may nominate right now.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 3
          },
          "title": {
            "type": "string",
            "description": "The program's name — the card heading.",
            "example": "Above & Beyond"
          },
          "name": {
            "type": "string",
            "description": "The same value as `title`, under the record's own attribute name, so a client matching this row against the dashboard's `active_programs` (which names it `name`) doesn't have to know the two keys differ.",
            "example": "Above & Beyond"
          },
          "slug": {
            "type": "string",
            "example": "above-beyond"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "example": "Recognition for going above and beyond job duties"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "inactive",
              "draft"
            ],
            "description": "Always `active` in this payload — the list serves active programs only. Present because the card paints a status pill.",
            "example": "active"
          },
          "status_label": {
            "type": "string",
            "description": "Display form of `status`, humanized server-side so both surfaces agree.",
            "example": "Active"
          },
          "program_type": {
            "type": "string",
            "enum": [
              "peer_to_peer",
              "manager_to_employee",
              "achievement_based",
              "milestone"
            ],
            "example": "achievement_based"
          },
          "program_type_label": {
            "type": "string",
            "description": "Display form of `program_type` — the card's type chip.",
            "example": "Achievement Awards"
          },
          "program_type_icon": {
            "type": "string",
            "description": "Font Awesome icon name the web chip uses (users / user-tie / trophy / flag-checkered / award), so the native chip carries the same glyph instead of maintaining its own mapping.",
            "example": "trophy"
          },
          "is_automatic": {
            "type": "boolean",
            "description": "True for a system-awarded milestone program. Swap the CTA for \"Awarded automatically — no nomination needed\" and hide the nominations stat, exactly as the web card does.",
            "example": false
          },
          "my_awards": {
            "type": "integer",
            "description": "Awards THIS viewer has received in this program. Active awards only — a revoked one is not something they hold, which is also how the dashboard tile and My Recognition count.",
            "example": 2
          },
          "my_nominations": {
            "type": "integer",
            "description": "Nominations THIS viewer has submitted to this program, in any status (pending, approved or rejected). Always 0 for an automatic program.",
            "example": 1
          },
          "starts_on": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the nomination window opens. Null means open-ended, the common case. Only in-window programs are listed, so a non-null value here is always in the past.",
            "example": "2026-07-01T00:00:00.000Z"
          },
          "ends_on": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the nomination window closes — for a client that wants to say \"closes in N days\". Null means open-ended.",
            "example": "2026-09-30T00:00:00.000Z"
          },
          "can_nominate": {
            "type": "boolean",
            "description": "Whether this viewer may file a nomination RIGHT NOW — the same answer the submit path gives. Hide the Nominate affordance when false.",
            "example": true
          },
          "nomination_block_reason": {
            "type": "string",
            "nullable": true,
            "description": "Why the CTA isn't offered, phrased for the person reading it. Null when `can_nominate` is true. Show it instead of a bare disabled button — it names what they can check or who to ask.",
            "example": "Quarterly Star Award is a Manager Recognition program — only managers can nominate in it. Recognize a teammate through a peer-to-peer program instead."
          },
          "reward_points": {
            "type": "object",
            "description": "This month's recognition budget for the program, in the POINTS employees see everywhere else in this app. Always present: an unbudgeted program (the common case) reports zeros and null dollars rather than omitting the block, so a client reads one shape.",
            "properties": {
              "has_budget": {
                "type": "boolean",
                "description": "Whether the program runs a monthly budget at all. Render no bar when false.",
                "example": true
              },
              "points_remaining": {
                "type": "integer",
                "description": "Reward points remaining this month. Can be NEGATIVE when a program has overspent its allowance — the figure stays honest; only `percent_remaining` clamps.",
                "example": 1000
              },
              "total_points": {
                "type": "integer",
                "description": "The program's total monthly reward-point allowance.",
                "example": 5000
              },
              "percent_remaining": {
                "type": "number",
                "format": "float",
                "description": "0–100, already clamped. Draw the bar from this rather than dividing the two figures yourself — an overspent program would otherwise produce a negative width.",
                "example": 20.0
              },
              "monthly_budget_dollars": {
                "type": "number",
                "format": "float",
                "nullable": true,
                "description": "`total_points` in the stored currency unit, for a consumer that reports dollars. Null when the program has no budget.",
                "example": 50.0
              },
              "remaining_budget_dollars": {
                "type": "number",
                "format": "float",
                "nullable": true,
                "description": "`points_remaining` in the stored currency unit.",
                "example": 10.0
              }
            }
          },
          "categories": {
            "type": "array",
            "description": "The program's ACTIVE categories, in the tenant's sort order — the card's chips, and the choices the nominate form will offer.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 9
                },
                "name": {
                  "type": "string",
                  "example": "Project Completion"
                },
                "slug": {
                  "type": "string",
                  "example": "project-completion"
                },
                "icon": {
                  "type": "string",
                  "description": "Font Awesome name; falls back to `star` when unconfigured.",
                  "example": "check-circle"
                },
                "color": {
                  "type": "string",
                  "description": "Hex colour; falls back to `#95a5a6` when unconfigured.",
                  "example": "#27ae60"
                }
              }
            }
          },
          "category_chip_limit": {
            "type": "integer",
            "description": "Where the web card collapses the rest into \"+N more\". Presentation guidance, not a rule — a narrower native card may break earlier — and `categories` always carries the full list, so expanding never needs a second request.",
            "example": 5
          }
        }
      },
      "RecognitionAwardCycleReview": {
        "allOf": [
          {
            "$ref": "#/components/schemas/RecognitionAwardCycleReviewCard"
          },
          {
            "type": "object",
            "description": "The committee review screen for ONE closed committee cycle: the `needs_my_review` card it was opened from, plus the anonymized pool, the committee roster and this caller's own ballot. Served whole — no pagination, because a submit replaces the entire ballot.",
            "required": [
              "committee",
              "committee_count",
              "nominees",
              "pool_count",
              "my_picks",
              "meta"
            ],
            "properties": {
              "committee": {
                "type": "array",
                "description": "Everyone on this cycle's review committee — the web page's \"Review committee: A and B\" line, as people rather than a pre-joined sentence. Shown so a member sees they aren't deciding alone. Includes the caller.",
                "items": {
                  "$ref": "#/components/schemas/RecognitionPerson"
                }
              },
              "committee_count": {
                "type": "integer",
                "example": 2
              },
              "nominees": {
                "type": "array",
                "description": "The pool, highest tally first. Complete — its length IS the total.",
                "items": {
                  "$ref": "#/components/schemas/RecognitionCycleNominee"
                }
              },
              "pool_count": {
                "type": "integer",
                "description": "Rows in `nominees`. May be lower than the card's `nominee_count` when somebody nominated has since left the business: they drop out of the pool rather than rendering a blank row, though a pick already saved for them stays valid.",
                "example": 3
              },
              "my_picks": {
                "type": "object",
                "description": "This caller's saved ballot for this cycle. Never anyone else's.",
                "required": [
                  "nominee_ids",
                  "count",
                  "submitted"
                ],
                "properties": {
                  "nominee_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "example": [
                      893
                    ]
                  },
                  "count": {
                    "type": "integer",
                    "example": 1
                  },
                  "submitted": {
                    "type": "boolean",
                    "description": "Whether this member has recorded a decision at all — so a client can say \"you haven't started\" rather than showing an empty ballot that looks the same as a deliberately-cleared one.",
                    "example": true
                  }
                }
              },
              "meta": {
                "$ref": "#/components/schemas/RecognitionAwardCycleReviewMeta"
              }
            }
          }
        ]
      },
      "RecognitionCycleNominee": {
        "type": "object",
        "description": "ONE nominee in the pool: who they are, how many colleagues put them forward, and a preview of what those colleagues wrote — never who wrote it.",
        "required": [
          "user",
          "nomination_count",
          "justifications",
          "additional_justification_count",
          "picked_by_me",
          "is_me",
          "can_pick"
        ],
        "properties": {
          "user": {
            "$ref": "#/components/schemas/RecognitionPerson"
          },
          "nomination_count": {
            "type": "integer",
            "description": "DISTINCT colleagues who nominated this person — the tally the pool is ordered by.",
            "example": 4
          },
          "justifications": {
            "type": "array",
            "description": "Anonymized reasons, oldest first, capped at `meta.justification_preview_limit`. Text and an opaque id ONLY: there is no nominator here and nothing that can be joined back to one.",
            "items": {
              "type": "object",
              "required": [
                "id",
                "text"
              ],
              "properties": {
                "id": {
                  "type": "integer",
                  "description": "Opaque row id, for list keying. It identifies the nomination, never its author.",
                  "example": 43
                },
                "text": {
                  "type": "string",
                  "example": "Christina keeps the team steady when the week goes sideways."
                }
              }
            }
          },
          "additional_justification_count": {
            "type": "integer",
            "description": "How many justifications the cap hid — the web row's \"…and N more nominations\" line. 0 when everything fits.",
            "example": 0
          },
          "picked_by_me": {
            "type": "boolean",
            "description": "Whether THIS caller's saved ballot includes this nominee.",
            "example": true
          },
          "is_me": {
            "type": "boolean",
            "description": "The caller's own row. The web renders it disabled with a \"You — recused\" caption.",
            "example": false
          },
          "can_pick": {
            "type": "boolean",
            "description": "False only for the caller's own row — you never vote for yourself, and the submit path strips the id even if it is sent. Do not offer a control when this is false.",
            "example": true
          }
        }
      },
      "RecognitionAwardCycleReviewMeta": {
        "type": "object",
        "description": "The review page's own copy and the one rule a client must not get wrong about the write, sent so the native screen says what the web screen says instead of inventing its own wording.",
        "required": [
          "title",
          "instructions",
          "anonymized",
          "justification_preview_limit",
          "submit_url",
          "submit_label",
          "submit_confirm",
          "submit_replaces_previous",
          "capabilities"
        ],
        "properties": {
          "title": {
            "type": "string",
            "example": "Committee review — Leadership Award — Q2"
          },
          "instructions": {
            "type": "string",
            "description": "The web page's subtitle, verbatim.",
            "example": "Nominations are shown anonymously (no nominator names). Pick the people you'd recognize — your choices are combined with the rest of the committee."
          },
          "anonymized": {
            "type": "boolean",
            "description": "Always true, and stated rather than implied: a client must not render nominator names on this screen, and there are none in the payload to render.",
            "example": true
          },
          "justification_preview_limit": {
            "type": "integer",
            "description": "The cap `justifications` is sliced to. Pair it with `additional_justification_count` instead of implying the list is complete.",
            "example": 4
          },
          "submit_url": {
            "type": "string",
            "description": "Absolute URL of this cycle's ballot endpoint.",
            "example": "https://acme.workforce.mangoapps.com/api/v1/recognitions/award_cycles/10/picks"
          },
          "submit_label": {
            "type": "string",
            "example": "Save my recommendations"
          },
          "submit_confirm": {
            "type": "string",
            "description": "The web page's confirmation prompt — it names the replace.",
            "example": "Submit your recommendations for this cycle? This replaces any picks you saved earlier."
          },
          "submit_replaces_previous": {
            "type": "boolean",
            "description": "Always true. The one thing a client must get right about the write: it is a replace, not an append. Send the whole selection.",
            "example": true
          },
          "empty_state": {
            "type": "object",
            "description": "Present **only** when the pool is empty — a committee cycle nobody nominated into. The cycle is still legitimately open for review; there is simply nothing to decide.",
            "required": [
              "key",
              "title"
            ],
            "properties": {
              "key": {
                "type": "string",
                "example": "nominees"
              },
              "title": {
                "type": "string",
                "example": "No nominations to review."
              }
            }
          },
          "capabilities": {
            "type": "object",
            "description": "The tenant toggles, so a client can hide the same surfaces the web nav hides.",
            "properties": {
              "award_cycles_enabled": {
                "type": "boolean",
                "example": true
              },
              "award_requests_enabled": {
                "type": "boolean",
                "example": true
              }
            }
          }
        }
      },
      "RecognitionCycleNominationForm": {
        "type": "object",
        "description": "The nominate composer: the cycle being nominated into, the form to fill, and the page's own copy.",
        "required": [
          "cycle",
          "form",
          "meta"
        ],
        "properties": {
          "cycle": {
            "allOf": [
              {
                "$ref": "#/components/schemas/RecognitionAwardCycleOpenCard"
              }
            ],
            "description": "The SAME card `GET /recognitions/award_cycles` serves under `open_now`, so a client that arrived from that list renders this screen from one model. Read `can_nominate` and `nomination_block_reason` from here — a closed or scheduled cycle still renders this screen, it just cannot be submitted."
          },
          "form": {
            "type": "object",
            "description": "Where to post, what the fields are called, and the bounds the model will enforce anyway — sent so a native client can validate before a round trip and fail in the same places the server does.",
            "required": [
              "submit_url",
              "submit_label",
              "nominee_search_url",
              "fields"
            ],
            "properties": {
              "submit_url": {
                "type": "string",
                "description": "Absolute URL to POST the nomination to.",
                "example": "https://acme.workforce.mangoapps.com/api/v1/recognitions/award_cycles/12/nominations"
              },
              "submit_label": {
                "type": "string",
                "example": "Submit nomination"
              },
              "nominee_search_url": {
                "type": "string",
                "description": "The typeahead that fills `nominee_id` — `GET /recognitions/employee_suggestions`. Named rather than duplicated: this endpoint serves no roster of its own.",
                "example": "https://acme.workforce.mangoapps.com/api/v1/recognitions/employee_suggestions"
              },
              "fields": {
                "type": "object",
                "properties": {
                  "nominee_id": {
                    "type": "object",
                    "properties": {
                      "label": {
                        "type": "string",
                        "example": "Who are you nominating?"
                      },
                      "placeholder": {
                        "type": "string",
                        "example": "Search for the person you want to nominate..."
                      },
                      "help_text": {
                        "type": "string",
                        "example": "You can't nominate yourself, and one nomination per person per award."
                      },
                      "required": {
                        "type": "boolean",
                        "example": true
                      }
                    }
                  },
                  "description": {
                    "type": "object",
                    "properties": {
                      "label": {
                        "type": "string",
                        "example": "Why are you nominating them?"
                      },
                      "hint": {
                        "type": "string",
                        "example": "(this is the signal — be specific)"
                      },
                      "placeholder": {
                        "type": "string",
                        "example": "What did they do, and what impact did it have? Tie it to the criteria above."
                      },
                      "required": {
                        "type": "boolean",
                        "example": true
                      },
                      "min_length": {
                        "type": "integer",
                        "example": 10
                      },
                      "max_length": {
                        "type": "integer",
                        "example": 1000
                      }
                    }
                  },
                  "title": {
                    "type": "object",
                    "properties": {
                      "required": {
                        "type": "boolean",
                        "example": false
                      },
                      "max_length": {
                        "type": "integer",
                        "example": 100
                      },
                      "default": {
                        "type": "string",
                        "description": "What the server stores when `title` is omitted — the cycle's name, truncated to `max_length`. Show it rather than leaving the field blank.",
                        "example": "Employee of the Quarter — Q3"
                      }
                    }
                  }
                }
              }
            }
          },
          "meta": {
            "type": "object",
            "description": "The page's own copy, so the native screen says what the web screen says.",
            "required": [
              "title",
              "allows_multiple_nominations",
              "capabilities"
            ],
            "properties": {
              "title": {
                "type": "string",
                "example": "Nominate for Employee of the Quarter — Q3"
              },
              "subtitle": {
                "type": "string",
                "nullable": true,
                "description": "The web page's subtitle, same wording and same date format.",
                "example": "Nominations close Sep 30, 2026."
              },
              "criteria_label": {
                "type": "string",
                "description": "Present only when the cycle states criteria — the callout's label above the form.",
                "example": "What we're looking for:"
              },
              "criteria": {
                "type": "string",
                "description": "What a good nomination looks like. Absent (not null) when the cycle states none, so a client can test the key itself.",
                "example": "Consistent, quiet leadership."
              },
              "allows_multiple_nominations": {
                "type": "boolean",
                "description": "Always true — a cycle POOLS nominations, so this is not a one-shot screen. Do NOT hide the CTA after one submit; the same nominator may come back for a different colleague. (A second nomination of the SAME person is what gets refused.)",
                "example": true
              },
              "capabilities": {
                "type": "object",
                "properties": {
                  "can_nominate": {
                    "type": "boolean",
                    "example": true
                  },
                  "award_cycles_enabled": {
                    "type": "boolean",
                    "example": true
                  },
                  "award_requests_enabled": {
                    "type": "boolean",
                    "example": true
                  }
                }
              }
            }
          }
        }
      },
      "RecognitionCycleNominationRequest": {
        "type": "object",
        "description": "The nomination. The `nomination` wrapper is optional — the four fields may be sent flat. `recognition_program_id` is NOT accepted: a cycle nomination belongs to the program running the cycle.",
        "properties": {
          "nomination": {
            "type": "object",
            "required": [
              "nominee_id",
              "description"
            ],
            "properties": {
              "nominee_id": {
                "type": "integer",
                "description": "The colleague being put forward. Must be an active member of this tenant, and cannot be the caller.",
                "example": 893
              },
              "description": {
                "type": "string",
                "minLength": 10,
                "maxLength": 1000,
                "description": "Why. The signal the cycle is decided on.",
                "example": "Rebuilt the onboarding checklist and cut new-hire ramp by a week."
              },
              "title": {
                "type": "string",
                "maxLength": 100,
                "description": "Optional. Defaults to the cycle's name, truncated.",
                "example": "The onboarding rebuild"
              },
              "is_public": {
                "type": "boolean",
                "description": "Optional. Defaults to the column default (public).",
                "example": true
              }
            }
          }
        }
      },
      "RecognitionCycleNominationResult": {
        "type": "object",
        "description": "The nomination as filed, plus the cycle card with it counted.",
        "required": [
          "id",
          "message",
          "nominee",
          "nominator",
          "title",
          "description",
          "status",
          "pooled",
          "cycle"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 4471
          },
          "message": {
            "type": "string",
            "description": "The web flash, verbatim — it names the person and the cycle.",
            "example": "Your nomination for Ada Lovelace has been added to Employee of the Quarter — Q3."
          },
          "nominee": {
            "$ref": "#/components/schemas/RecognitionPerson"
          },
          "nominator": {
            "$ref": "#/components/schemas/RecognitionPerson"
          },
          "title": {
            "type": "string",
            "example": "Employee of the Quarter — Q3"
          },
          "description": {
            "type": "string",
            "example": "Rebuilt the onboarding checklist and cut new-hire ramp by a week."
          },
          "status": {
            "type": "string",
            "description": "Always `pending` on this path, and that is the point — see `pooled`.",
            "enum": [
              "pending"
            ],
            "example": "pending"
          },
          "pooled": {
            "type": "boolean",
            "description": "Always true. A cycle nomination is POOLED, not reviewed: no reviewer is assigned, no Award is minted, and there is no per-nomination approve/reject. The cycle decides when it closes. Do NOT render the Model A \"awaiting approval\" affordance against this row.",
            "example": true
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time"
          },
          "is_public": {
            "type": "boolean",
            "example": true
          },
          "is_anonymous": {
            "type": "boolean",
            "description": "False unless the tenant allows anonymity. Cycle nominations are attributed by default.",
            "example": false
          },
          "cycle": {
            "allOf": [
              {
                "$ref": "#/components/schemas/RecognitionAwardCycleOpenCard"
              }
            ],
            "description": "The cycle AFTER the write, so `nomination_count`, `nominee_count`, `nominated_by_me` and `my_nomination_count` already reflect this submission. Re-render the row the client came from off THIS rather than re-fetching the list."
          }
        }
      },
      "RecognitionCycleNominationRefusal": {
        "type": "object",
        "description": "Every refusal from either verb. `error.details` always names the cycle and its RECONCILED state, because two of the refusals are about that state and the client's next move is to re-render the card.",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "access_denied",
                  "feature_disabled",
                  "giving_not_allowed",
                  "cycle_not_open",
                  "invalid_nominee",
                  "validation_failed",
                  "content_rejected",
                  "nomination_failed"
                ],
                "example": "cycle_not_open"
              },
              "message": {
                "type": "string",
                "description": "The web page's own wording for the same refusal.",
                "example": "That award isn't open for nominations right now."
              },
              "details": {
                "type": "object",
                "properties": {
                  "cycle_id": {
                    "type": "integer",
                    "example": 12
                  },
                  "status": {
                    "type": "string",
                    "description": "The cycle's EFFECTIVE status, not its stored column — a cycle stored `scheduled` whose window has opened reads `open` here, and an admin-forced close reads `closed` while `closes_at` is still in the future.",
                    "enum": [
                      "scheduled",
                      "open",
                      "closed",
                      "decided",
                      "archived"
                    ],
                    "example": "scheduled"
                  },
                  "errors": {
                    "type": "array",
                    "description": "`validation_failed` / `content_rejected` only — the messages, as one list.",
                    "items": {
                      "type": "string"
                    },
                    "example": [
                      "You've already nominated this person for this award."
                    ]
                  },
                  "field_errors": {
                    "type": "object",
                    "description": "`validation_failed` / `content_rejected` only — per-field messages, so a native form can mark the offending field instead of parsing a sentence. A content refusal names the field whose text was rejected, e.g. `{\"description\": [\"contains inappropriate language or terms that aren't allowed\"]}`.",
                    "additionalProperties": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "example": {
                      "nominee": [
                        "cannot be the same as the nominator"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      },
      "RecognitionCyclePicksRequest": {
        "type": "object",
        "description": "The member's COMPLETE ballot for this cycle. Supply `nominee_ids` OR `nominee_id` — sending neither is refused with 422 `no_selection` rather than read as \"clear everything\".",
        "properties": {
          "nominee_ids": {
            "description": "Every nominee this member recommends. An array of ids, or the web's comma-separated string form. `[]` CLEARS the ballot. This replaces the saved set — it is not a delta.",
            "oneOf": [
              {
                "type": "array",
                "items": {
                  "type": "integer"
                }
              },
              {
                "type": "string"
              }
            ],
            "example": [
              893,
              265
            ]
          },
          "nominee_id": {
            "type": "integer",
            "description": "Single-pick shorthand — identical to sending a one-element `nominee_ids`, including the replace. Ignored when `nominee_ids` is present.",
            "example": 893
          }
        }
      },
      "RecognitionCyclePicksResult": {
        "type": "object",
        "description": "The member's ballot AFTER the write. Re-render from this, not from what was sent.",
        "required": [
          "cycle_id",
          "cycle_name",
          "message",
          "picked_ids",
          "picked_count",
          "picked",
          "ignored_ids",
          "previous_picked_ids",
          "changed"
        ],
        "properties": {
          "cycle_id": {
            "type": "integer",
            "example": 10
          },
          "cycle_name": {
            "type": "string",
            "example": "Leadership Award — Q2"
          },
          "message": {
            "type": "string",
            "description": "The web flash, verbatim — including the honest empty case, which says \"cleared\" rather than \"saved\".",
            "example": "Your recommendations for Leadership Award — Q2 were saved."
          },
          "picked_ids": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "description": "The saved ballot. Authoritative.",
            "example": [
              893,
              265
            ]
          },
          "picked_count": {
            "type": "integer",
            "example": 2
          },
          "picked": {
            "type": "array",
            "description": "The people behind `picked_ids`, name-ordered, for an immediate toast or summary.",
            "items": {
              "$ref": "#/components/schemas/RecognitionPerson"
            }
          },
          "ignored_ids": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "description": "Ids that were sent but could not be picked — not in this cycle's pool, or the caller themselves. Empty on a well-formed call; a non-empty one means the client's list is stale.",
            "example": []
          },
          "previous_picked_ids": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "description": "The ballot this call replaced.",
            "example": [
              893
            ]
          },
          "changed": {
            "type": "boolean",
            "description": "False when the submitted ballot matched the saved one — skip the \"saved\" toast on a no-op re-submit.",
            "example": true
          },
          "review_url": {
            "type": "string",
            "description": "Absolute URL of the web review page for this cycle.",
            "example": "https://acme.workforce.mangoapps.com/recognition/award-cycles/10/review"
          }
        }
      },
      "RecognitionCycleReviewRefusal": {
        "type": "object",
        "description": "A committee-review refusal. The message is the WEB page's own flash wording — both surfaces read it from one place, so they cannot drift.",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "access_denied",
                  "feature_disabled",
                  "not_committee_cycle",
                  "not_committee_member",
                  "cycle_not_closed",
                  "recused"
                ],
                "example": "recused"
              },
              "message": {
                "type": "string",
                "example": "You're nominated in this cycle, so you can't take part in the decision."
              },
              "details": {
                "type": "object",
                "description": "Present on the four gate refusals (not on `access_denied` / `feature_disabled`, which are about the tenant, not the cycle).",
                "properties": {
                  "cycle_id": {
                    "type": "integer",
                    "example": 10
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "scheduled",
                      "open",
                      "closed",
                      "decided",
                      "archived"
                    ],
                    "example": "closed"
                  }
                }
              }
            }
          }
        }
      },
      "RecognitionReactionState": {
        "type": "object",
        "description": "One recognition's reaction state AFTER a write — the same keys the `engagement` block carries on `GET /recognitions/feed` and `GET /recognitions/posts/{id}`, so a client patches the card it already holds instead of re-fetching the screen.",
        "required": [
          "item_type",
          "item_id",
          "emoji",
          "reacted",
          "reactions_count",
          "reaction_summary",
          "my_reactions"
        ],
        "properties": {
          "item_type": {
            "type": "string",
            "enum": [
              "recognition_post",
              "award",
              "recognition_comment"
            ],
            "description": "The CANONICAL type, even when the request used an alias.",
            "example": "recognition_post"
          },
          "item_id": {
            "type": "integer",
            "example": 4471
          },
          "emoji": {
            "type": "string",
            "description": "The glyph this call toggled.",
            "example": "🎉"
          },
          "reacted": {
            "type": "boolean",
            "description": "True when this call ADDED the reaction (HTTP 201), false when it removed one (HTTP 200).",
            "example": true
          },
          "reactions_enabled": {
            "type": "boolean",
            "description": "The tenant's reaction switch, so a client can retire the bar in step with it.",
            "example": true
          },
          "reactions_count": {
            "type": "integer",
            "description": "Every reaction on this recognition, from every person.",
            "example": 7
          },
          "reaction_summary": {
            "type": "array",
            "description": "Grouped counts, most-reacted first (the glyph breaks a tie).",
            "items": {
              "type": "object",
              "properties": {
                "emoji": {
                  "type": "string",
                  "example": "👍"
                },
                "count": {
                  "type": "integer",
                  "example": 4
                }
              }
            }
          },
          "my_reaction": {
            "type": "string",
            "nullable": true,
            "description": "The caller's first emoji on this recognition — the single-glyph convenience the feed and detail cards render. Null when they hold none.",
            "example": "🎉"
          },
          "my_reactions": {
            "type": "array",
            "description": "EVERY emoji this caller holds on this recognition. A viewer may hold several at once, each toggled independently.",
            "items": {
              "type": "string"
            },
            "example": [
              "🎉",
              "🔥"
            ]
          },
          "unread_notification_count": {
            "type": "integer",
            "example": 3
          }
        }
      },
      "RecognitionReactor": {
        "type": "object",
        "description": "One reaction with the person who left it, as `GET /recognitions/reactions` pages them.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 90210
          },
          "emoji": {
            "type": "string",
            "example": "🎉"
          },
          "label": {
            "type": "string",
            "description": "The friendly name for the glyph, for tooltips and screen readers.",
            "example": "Celebrate"
          },
          "user": {
            "$ref": "#/components/schemas/RecognitionPerson"
          },
          "reacted_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RecognitionReactionSummary": {
        "type": "object",
        "description": "The per-emoji rollup a reactor sheet's tabs are built from, shipped alongside the paged list on `GET /recognitions/reactions` and `GET /recognitions/comments/{id}/reactions`.\nComputed over the **whole set**, not the page, and **not** narrowed by `?emoji=` — the tabs must keep showing the glyphs the reader can switch *to*, and their counts must not shrink as the reader pages. Two aggregate queries, so a thousand reactions cost the same as three.\nSame keys as the `engagement` block on `GET /recognitions/feed` and `GET /recognitions/posts/{id}`, so a client has one shape to parse.",
        "required": [
          "reactions_count",
          "reaction_summary",
          "my_reactions",
          "reactions_enabled"
        ],
        "properties": {
          "reactions_count": {
            "type": "integer",
            "description": "Every reaction on this item, from every person.",
            "example": 7
          },
          "reaction_summary": {
            "type": "array",
            "description": "Grouped counts, most-reacted first (the glyph breaks a tie).",
            "items": {
              "type": "object",
              "properties": {
                "emoji": {
                  "type": "string",
                  "example": "👍"
                },
                "count": {
                  "type": "integer",
                  "example": 4
                }
              }
            }
          },
          "my_reaction": {
            "type": "string",
            "nullable": true,
            "description": "The caller's first emoji on this item — the single-glyph convenience the feed and detail cards render. Null when they hold none.",
            "example": "🎉"
          },
          "my_reactions": {
            "type": "array",
            "description": "EVERY emoji this caller holds on this item.",
            "items": {
              "type": "string"
            },
            "example": [
              "🎉",
              "🔥"
            ]
          },
          "reactions_enabled": {
            "type": "boolean",
            "description": "The tenant's reaction switch. The list itself is deliberately NOT gated on it (turning reactions off retires the affordance, it does not retract what people left), so this is how a client reading only this endpoint knows whether to render the bar.",
            "example": true
          }
        }
      },
      "RecognitionReactionsPage": {
        "type": "object",
        "description": "The page envelope on `GET /recognitions/reactions`.",
        "properties": {
          "current_page": {
            "type": "integer",
            "example": 1
          },
          "per_page": {
            "type": "integer",
            "example": 25
          },
          "total_count": {
            "type": "integer",
            "example": 7
          },
          "total_pages": {
            "type": "integer",
            "example": 1
          },
          "has_next_page": {
            "type": "boolean",
            "example": false
          },
          "has_prev_page": {
            "type": "boolean",
            "example": false
          }
        }
      },
      "RecognitionAcknowledgement": {
        "type": "object",
        "description": "The answer from `POST /recognitions/{posts,awards}/{id}/acknowledge`. Always a `200`: \"you are not the person recognized\" is the ordinary case on a shared card, not an error.",
        "required": [
          "type",
          "id",
          "acknowledged",
          "newly_acknowledged"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "recognition_post",
              "award"
            ],
            "description": "The same vocabulary `unacknowledged_recognitions` emits on `GET /api/v1/apps`, so the `type` a client is given is the one it reads back.",
            "example": "recognition_post"
          },
          "id": {
            "type": "integer",
            "description": "The row that was stamped. For a GROUP give this is the caller's OWN sibling row, which may differ from the id in the path.",
            "example": 4471
          },
          "acknowledged": {
            "type": "boolean",
            "description": "Whether this recognition is settled — `true` for a repeat call, and `false` when the caller is not the person recognized (nothing was written).",
            "example": true
          },
          "newly_acknowledged": {
            "type": "boolean",
            "description": "Whether THIS call settled it. The flag to key the animation off: a re-opened card never re-celebrates.",
            "example": true
          },
          "acknowledged_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When it was acknowledged; `null` when nothing was written.",
            "example": "2026-08-18T09:14:22Z"
          },
          "reason": {
            "type": "string",
            "enum": [
              "not_recipient",
              "not_active",
              "invalid_item",
              "error"
            ],
            "description": "Present only when `acknowledged` is `false`. DIAGNOSTIC, never a message to show a user — `not_recipient` is the ordinary answer whenever somebody other than the person recognized opens a card.",
            "example": "not_recipient"
          }
        }
      },
      "RecognitionFeedItem": {
        "type": "object",
        "description": "One recognition card. The identity half (type, id, title, message, points, people, program, value, timestamp) is shared with the dashboard's activity rows; the rest is what the browsable feed card renders on top.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "award",
              "recognition_post"
            ],
            "description": "`award` is a formal program award; `recognition_post` is peer recognition. Clients use this to pick the card treatment."
          },
          "id": {
            "type": "integer",
            "example": 8814
          },
          "title": {
            "type": "string",
            "nullable": true,
            "description": "Award title. Null for peer posts, which lead with the message.",
            "example": "Excellence Award"
          },
          "message": {
            "type": "string",
            "nullable": true,
            "description": "The recognition text — an award's description, or a post's content.",
            "example": "Thank you for the incredible help on the release deadline."
          },
          "points": {
            "type": "integer",
            "description": "Reward points the RECIPIENT earned. An award stores dollars and converts; a peer post stores points directly. Always present (0 when none).",
            "example": 250
          },
          "recipient": {
            "$ref": "#/components/schemas/RecognitionPerson"
          },
          "group_recipients": {
            "type": "array",
            "nullable": true,
            "description": "Present only on a collapsed GROUP give — every recipient of the one recognition, so a client can render \"Alice, Bob and 3 others\". Null for an ordinary single-recipient card.",
            "items": {
              "$ref": "#/components/schemas/RecognitionPerson"
            }
          },
          "giver": {
            "allOf": [
              {
                "$ref": "#/components/schemas/RecognitionPerson"
              }
            ],
            "nullable": true,
            "description": "Null when `anonymous` is true. For an automated lifecycle award this is the \"System (Automated)\" pseudo-person with a null id — the system principal is never exposed."
          },
          "anonymous": {
            "type": "boolean",
            "description": "The giver chose to stay unattributed; `giver` is null.",
            "example": false
          },
          "program": {
            "type": "string",
            "nullable": true,
            "example": "Spot Award"
          },
          "category": {
            "type": "string",
            "nullable": true,
            "example": "Project Completion"
          },
          "company_value": {
            "type": "string",
            "nullable": true,
            "description": "The company value this recognition celebrates.",
            "example": "Teamwork"
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the recognition happened (an award's awarded_at, a post's published_at)."
          },
          "visibility": {
            "type": "string",
            "nullable": true,
            "enum": [
              "public",
              "department",
              "team",
              "private"
            ],
            "description": "Audience of a peer post. Null for awards, which are public by definition in this feed."
          },
          "tags": {
            "type": "array",
            "description": "Free-form recognition tags. Always an array (empty when none).",
            "items": {
              "type": "string"
            },
            "example": [
              "teamwork",
              "excellence"
            ]
          },
          "photo_url": {
            "type": "string",
            "nullable": true,
            "description": "Absolute URL of the photo the giver attached, as a resized WebP rendition (never the original upload). Null when there is none."
          },
          "award_art_url": {
            "type": "string",
            "nullable": true,
            "description": "Absolute URL of the award-card artwork, when one was chosen."
          },
          "engagement": {
            "type": "object",
            "description": "Reaction / comment / share counts, plus the caller's own reaction.",
            "properties": {
              "reactions_count": {
                "type": "integer",
                "example": 4
              },
              "reactions_enabled": {
                "type": "boolean",
                "description": "Tenant setting — hide the reaction affordance when false."
              },
              "my_reaction": {
                "type": "string",
                "nullable": true,
                "description": "The emoji the caller reacted with, or null.",
                "example": "🎉"
              },
              "reaction_summary": {
                "type": "array",
                "description": "Per-emoji totals, most-used first.",
                "items": {
                  "type": "object",
                  "properties": {
                    "emoji": {
                      "type": "string",
                      "example": "🎉"
                    },
                    "count": {
                      "type": "integer",
                      "example": 3
                    }
                  }
                }
              },
              "comments_count": {
                "type": "integer",
                "example": 2
              },
              "comments_enabled": {
                "type": "boolean",
                "description": "Tenant setting — hide the comment affordance when false."
              },
              "shares_count": {
                "type": "integer",
                "example": 0
              }
            }
          },
          "boost": {
            "type": "object",
            "nullable": true,
            "description": "Pile-on points — teammates adding their own allowance to someone else's recognition. **Null for awards**, which cannot be boosted, so clients should treat null as \"no boost affordance\" rather than zero.",
            "properties": {
              "total_points": {
                "type": "integer",
                "description": "Points teammates have piled on so far.",
                "example": 40
              },
              "boosted_by_me": {
                "type": "boolean",
                "description": "A boost is one per person; true means the caller already did."
              },
              "can_boost": {
                "type": "boolean",
                "description": "False on the caller's own give or receipt, when they have already boosted, when peer points are off, or when their remaining allowance covers none of the offered amounts."
              },
              "amounts": {
                "type": "array",
                "description": "Amounts the server will actually accept, already filtered to the caller's remaining allowance. Empty when they cannot boost.",
                "items": {
                  "type": "integer"
                },
                "example": [
                  5,
                  10,
                  25
                ]
              }
            }
          },
          "permissions": {
            "type": "object",
            "description": "What this caller may do with this item, matching what the server would allow. This is where an admin's feed differs from an employee's — everything else is identical.",
            "properties": {
              "can_delete": {
                "type": "boolean",
                "description": "True for the post's author, a business admin, or a Recognitions app admin."
              },
              "can_edit": {
                "type": "boolean",
                "description": "What PATCH /recognitions/posts|awards/{id} would accept: the author or a moderator on a post, a moderator on an award, and in both cases only while the recognition is still active. NOT the same gate as can_delete — a held post is deletable but not editable, and an award is editable by a moderator while can_delete is withheld on a feed row."
              },
              "can_share": {
                "type": "boolean",
                "description": "Public, non-anonymous, active items only."
              },
              "can_comment": {
                "type": "boolean"
              },
              "can_react": {
                "type": "boolean"
              },
              "can_boost": {
                "type": "boolean",
                "description": "Mirrors `boost.can_boost`; always false for awards."
              }
            }
          }
        }
      },
      "RecognitionFeedMeta": {
        "type": "object",
        "description": "Paging state, the active filter, and viewer-level capabilities.",
        "properties": {
          "filter": {
            "type": "string",
            "enum": [
              "all",
              "awards",
              "posts",
              "my_team",
              "my_department"
            ],
            "description": "The filter actually applied (an unknown value falls back to `all`)."
          },
          "available_filters": {
            "type": "array",
            "description": "Every filter this endpoint accepts, in the order the web page shows them.",
            "items": {
              "type": "string"
            },
            "example": [
              "all",
              "awards",
              "posts",
              "my_team",
              "my_department"
            ]
          },
          "current_page": {
            "type": "integer",
            "example": 1
          },
          "per_page": {
            "type": "integer",
            "example": 20
          },
          "has_next_page": {
            "type": "boolean",
            "description": "**The authoritative paging signal — always exact.** Page until this is false; do not drive paging from the totals below."
          },
          "has_prev_page": {
            "type": "boolean"
          },
          "total_count": {
            "type": "integer",
            "description": "Number of items in the loaded window after filtering. The feed merges two tables and applies some filters in memory, so an exact total would mean loading everything — read `total_count_exact` before showing this.",
            "example": 37
          },
          "total_count_exact": {
            "type": "boolean",
            "description": "True when `total_count` is the real total; false when it is only a lower bound because the window was truncated. **Do not render \"of N\" unless this is true.**"
          },
          "total_pages": {
            "type": "integer",
            "nullable": true,
            "description": "Null whenever `total_count_exact` is false — a null instead of a confidently wrong page count.",
            "example": 2
          },
          "capped_at": {
            "type": "integer",
            "description": "Maximum rows read per source. The feed will not page beyond this many items even if more exist in the 30-day window.",
            "example": 200
          },
          "capabilities": {
            "type": "object",
            "description": "Viewer-level state the client needs to render the feed's global actions, so it never offers something the server would refuse.",
            "properties": {
              "can_give_recognition": {
                "type": "boolean",
                "description": "Whether this caller may give peer recognition at all."
              },
              "boosting_available": {
                "type": "boolean",
                "description": "Peer points are enabled AND this caller may give."
              },
              "giving_remaining": {
                "type": "integer",
                "description": "Points left in the caller's monthly giving allowance.",
                "example": 120
              },
              "reactions_enabled": {
                "type": "boolean"
              },
              "comments_enabled": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "RecognitionLeaderboardRow": {
        "type": "object",
        "required": [
          "rank",
          "user_id",
          "user",
          "name",
          "count",
          "is_me"
        ],
        "properties": {
          "rank": {
            "type": "integer",
            "description": "1-based POSITION in this board — what the medallion renders (gold #1, silver #2, bronze #3, neutral from #4). Not the competition-style rank in `my_standing`: tied rows occupy consecutive positions here.",
            "example": 1
          },
          "user_id": {
            "type": "integer",
            "example": 1033
          },
          "user": {
            "type": "object",
            "nullable": true,
            "description": "Null when the person has left the business since being recognized. The row is KEPT (with `name` degrading to \"Unknown\") rather than vanishing and shifting every rank below it.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 1033
              },
              "name": {
                "type": "string",
                "example": "Weston D'Amore"
              },
              "title": {
                "type": "string",
                "nullable": true,
                "description": "Job title. The native row shows title, falling back to department.",
                "example": "Area Manager"
              },
              "department": {
                "type": "string",
                "nullable": true,
                "example": "Sales Department"
              },
              "image": {
                "type": "string",
                "nullable": true,
                "description": "Absolute avatar URL."
              }
            }
          },
          "name": {
            "type": "string",
            "description": "The display name for the row. \"Unknown\" when `user` is null, so the row still renders.",
            "example": "Weston D'Amore"
          },
          "count": {
            "type": "integer",
            "description": "Recognition received (recipients) or given (givers) in the window, under the reported `counting_basis`.",
            "example": 6
          },
          "is_me": {
            "type": "boolean",
            "description": "Whether this row is the caller — the \"You\" badge.",
            "example": false
          }
        }
      },
      "RecognitionLeaderboardStanding": {
        "type": "object",
        "required": [
          "rank",
          "count",
          "in_top",
          "ranked_total"
        ],
        "properties": {
          "rank": {
            "type": "integer",
            "nullable": true,
            "description": "Competition-style 1-based rank against the FULL board — ties SHARE a rank. Null when the caller has no activity in the window: unranked, not last. Render nothing rather than a zero row.",
            "example": 7
          },
          "count": {
            "type": "integer",
            "description": "The caller's own count in the window. 0 when unranked.",
            "example": 1
          },
          "in_top": {
            "type": "boolean",
            "description": "Whether the caller already appears in the returned rows. When false and `rank` is present, pin a \"You — #N\" footer row; when true, don't, or it duplicates a row already drawn.",
            "example": false
          },
          "ranked_total": {
            "type": "integer",
            "description": "How many people appear on the full board — the denominator behind `rank`, so \"#7\" can be shown as \"#7 of 19\" without a second request.",
            "example": 19
          }
        }
      },
      "RecognitionTeamCountTile": {
        "type": "object",
        "description": "One of the two count tiles. `total` IS `awards + shout_outs` — render the total large and the split as the sub-line, not three separate numbers.",
        "properties": {
          "total": {
            "type": "integer",
            "example": 34
          },
          "awards": {
            "type": "integer",
            "description": "Formal awards (dollar-denominated on the record).",
            "example": 12
          },
          "shout_outs": {
            "type": "integer",
            "description": "Peer recognition posts. Every post counts, including 0-point ones — a tenant not using redeemable rewards collects no points at all.",
            "example": 22
          }
        }
      },
      "RecognitionTeamGivingPool": {
        "type": "object",
        "description": "One pool the viewer can give from. Points are pre-converted, so a client never needs the tenant's points-per-dollar rate.",
        "properties": {
          "label": {
            "type": "string",
            "example": "Spot Awards"
          },
          "sublabel": {
            "type": "string",
            "example": "Manager to employee budget"
          },
          "kind": {
            "type": "string",
            "enum": [
              "allowance",
              "budget"
            ],
            "description": "`allowance` is the monthly peer-giving pool; `budget` is an annual award-program allocation. The two never draw on each other.",
            "example": "budget"
          },
          "available_points": {
            "type": "integer",
            "example": 1500
          },
          "allocated_points": {
            "type": "integer",
            "example": 5000
          },
          "percent_remaining": {
            "type": "integer",
            "description": "`available / allocated`, clamped to 0..100 — a manager who overspent a budget must not drive a negative-width progress bar.",
            "example": 30
          },
          "period": {
            "type": "string",
            "description": "The window the allocation covers — an allowance resets monthly, a budget annually.",
            "example": "this year"
          }
        }
      },
      "RecognitionTeamPendingNomination": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 88
          },
          "title": {
            "type": "string",
            "example": "Renewal save of the quarter"
          },
          "nominee": {
            "$ref": "#/components/schemas/RecognitionPerson"
          },
          "nominator": {
            "$ref": "#/components/schemas/RecognitionPerson"
          },
          "program": {
            "type": "string",
            "nullable": true,
            "example": "Spot Awards"
          },
          "status": {
            "type": "string",
            "example": "pending"
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RecognitionApprovalsPageMeta": {
        "type": "object",
        "description": "Kaminari paging state for one of the two approvals lists. Counts are exact (each list is one business-scoped relation), so `total_pages` is always a real number and `has_next_page` is authoritative.",
        "properties": {
          "current_page": {
            "type": "integer",
            "example": 1
          },
          "per_page": {
            "type": "integer",
            "example": 20
          },
          "total_count": {
            "type": "integer",
            "example": 37
          },
          "total_pages": {
            "type": "integer",
            "example": 2
          },
          "has_next_page": {
            "type": "boolean"
          },
          "has_prev_page": {
            "type": "boolean"
          }
        }
      },
      "RecognitionApprovalNomination": {
        "type": "object",
        "description": "One nomination awaiting the reviewer's decision.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 88
          },
          "title": {
            "type": "string",
            "example": "Renewal save of the quarter"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "example": "Turned a churn risk into a multi-year renewal."
          },
          "nominee": {
            "$ref": "#/components/schemas/RecognitionPerson"
          },
          "nominator": {
            "allOf": [
              {
                "$ref": "#/components/schemas/RecognitionPerson"
              }
            ],
            "nullable": true,
            "description": "Withheld (null) when the nomination is anonymous."
          },
          "anonymous": {
            "type": "boolean",
            "description": "True when the nominator is withheld."
          },
          "program": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer",
                "example": 5
              },
              "name": {
                "type": "string",
                "example": "Spot Awards"
              }
            }
          },
          "category": {
            "type": "object",
            "nullable": true,
            "description": "The award category, with the chip's icon/color fallbacks. Null when none is set.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 12
              },
              "name": {
                "type": "string",
                "example": "Above and Beyond"
              },
              "icon": {
                "type": "string",
                "example": "fa-star"
              },
              "color": {
                "type": "string",
                "example": "#f59e0b"
              }
            }
          },
          "requested_points": {
            "type": "integer",
            "nullable": true,
            "description": "The requested award value in reward points (dollars converted server-side). Null when no value was requested.",
            "example": 250
          },
          "status": {
            "type": "string",
            "example": "pending",
            "description": "`pending` or `under_review`."
          },
          "multi_level_approval": {
            "type": "boolean",
            "description": "True when the program needs more than one approval level."
          },
          "approval_progress": {
            "type": "object",
            "description": "Which approval step this nomination is on, of how many.",
            "properties": {
              "current": {
                "type": "integer",
                "example": 1
              },
              "required": {
                "type": "integer",
                "example": 1
              }
            }
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "supporting_documents": {
            "type": "object",
            "description": "The reviewer's evidence. `count` is the ledger size; `files` reads through to the live blobs; `missing_count` is ledger rows whose bytes are gone (surfaced so a shrinking list is visible, not silent).",
            "properties": {
              "count": {
                "type": "integer",
                "example": 2
              },
              "missing_count": {
                "type": "integer",
                "example": 0
              },
              "files": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "filename": {
                      "type": "string",
                      "example": "renewal-contract.pdf"
                    },
                    "content_type": {
                      "type": "string",
                      "example": "application/pdf"
                    },
                    "byte_size": {
                      "type": "integer",
                      "example": 84213
                    },
                    "icon": {
                      "type": "string",
                      "example": "fa-file-pdf"
                    },
                    "download_url": {
                      "type": "string",
                      "description": "Absolute, attachment-disposition download URL for the blob."
                    }
                  }
                }
              }
            }
          }
        }
      },
      "RecognitionApprovalPost": {
        "type": "object",
        "description": "One peer recognition post held for manager approval.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 401
          },
          "message": {
            "type": "string",
            "example": "Thanks for covering the late shift."
          },
          "author": {
            "allOf": [
              {
                "$ref": "#/components/schemas/RecognitionPerson"
              }
            ],
            "nullable": true,
            "description": "Withheld (null) when the post is anonymous."
          },
          "anonymous": {
            "type": "boolean"
          },
          "recipient": {
            "$ref": "#/components/schemas/RecognitionPerson"
          },
          "points": {
            "type": "integer",
            "description": "The peer points attached to the post (already in points; 0 when none).",
            "example": 500
          },
          "has_award_art": {
            "type": "boolean",
            "description": "The post carries illustrated award art (an Asset Library card)."
          },
          "has_attachments": {
            "type": "boolean",
            "description": "The post carries a photo or file attachments."
          },
          "company_value": {
            "type": "string",
            "nullable": true,
            "example": "Customer First"
          },
          "reviewer": {
            "allOf": [
              {
                "$ref": "#/components/schemas/RecognitionPerson"
              }
            ],
            "nullable": true,
            "description": "The reviewer the post routes to (useful when an admin views the whole tenant's held posts)."
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RecognitionTeamGapRow": {
        "type": "object",
        "description": "A team member nobody has recognized lately.",
        "properties": {
          "user": {
            "$ref": "#/components/schemas/RecognitionPerson"
          },
          "days_since_recognition": {
            "type": "integer",
            "nullable": true,
            "description": "Null when this person has NEVER been recognized. Render \"Never recognized\" for that case — never \"0 days ago\".",
            "example": 47
          },
          "last_recognized_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Null alongside a null `days_since_recognition`."
          },
          "never_recognized": {
            "type": "boolean",
            "description": "True exactly when `days_since_recognition` is null.",
            "example": false
          }
        }
      },
      "RecognitionTeamAnniversary": {
        "type": "object",
        "properties": {
          "user": {
            "$ref": "#/components/schemas/RecognitionPerson"
          },
          "job_title": {
            "type": "string",
            "nullable": true,
            "example": "Support Specialist"
          },
          "department": {
            "type": "string",
            "description": "The person's department, or \"Unassigned\".",
            "example": "Customer Success"
          },
          "years": {
            "type": "integer",
            "description": "Years of service they are about to complete. Always >= 1.",
            "example": 5
          },
          "milestone_label": {
            "type": "string",
            "description": "Ready-to-render label — \"Decade\", \"Silver (25)\", \"3 Years\".",
            "example": "5 Years"
          },
          "anniversary_date": {
            "type": "string",
            "format": "date"
          },
          "days_until": {
            "type": "integer",
            "description": "0 means today.",
            "example": 12
          },
          "recognized": {
            "type": "boolean",
            "description": "Whether their years-of-service award was already given this year. The web hides the Celebrate button when true — offering it again invites a duplicate award.",
            "example": false
          }
        }
      },
      "RecognitionAnniversaryRosterRow": {
        "type": "object",
        "description": "ONE roster row — the same information the web table prints, minus its link chrome. Richer than `RecognitionTeamAnniversary` (the Team screen's teaser), which carries no hire date, tier, status or action flag.",
        "properties": {
          "user": {
            "$ref": "#/components/schemas/RecognitionPerson"
          },
          "job_title": {
            "type": "string",
            "description": "The resolved title the row prints: the person's job title, falling back to their org role and then \"No title\". (`user.title` is the raw column and may be null.)",
            "example": "Support Specialist"
          },
          "department": {
            "type": "string",
            "description": "The person's department, or \"Unassigned\".",
            "example": "Customer Success"
          },
          "hire_date": {
            "type": "string",
            "format": "date",
            "description": "The date the anniversary counts from — their original hire date when one is recorded, so a rehire keeps their full service length."
          },
          "anniversary_date": {
            "type": "string",
            "format": "date",
            "description": "The upcoming anniversary itself, inside the applied window."
          },
          "years": {
            "type": "integer",
            "description": "Years of service they are about to complete. Always >= 1.",
            "example": 5
          },
          "milestone_label": {
            "type": "string",
            "description": "Ready-to-render label — \"Decade\", \"Silver (25)\", \"3 Years\".",
            "example": "5 Years"
          },
          "milestone_tier": {
            "type": "string",
            "enum": [
              "early_career",
              "established",
              "veteran",
              "senior",
              "legendary"
            ],
            "description": "The service-length band, so a client can tint the year chip the way the web badge and its legend do without re-deriving the boundaries (1-4 / 5-9 / 10-19 / 20-24 / 25+).",
            "example": "established"
          },
          "milestone_tier_label": {
            "type": "string",
            "description": "The band's display name, matching the web legend.",
            "example": "Established"
          },
          "days_until": {
            "type": "integer",
            "description": "0 means today. **Signed** — negative when `from_date` is in the past, so render \"3 days ago\" rather than an impossible countdown.",
            "example": 12
          },
          "is_upcoming_soon": {
            "type": "boolean",
            "description": "Within a week and not yet past — the rows the web table tints green.",
            "example": false
          },
          "recognized": {
            "type": "boolean",
            "description": "Whether their years-of-service award was already given this year.",
            "example": false
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "recognized",
              "rewarded"
            ],
            "description": "The web badge's three states. `rewarded` means the gift/fulfilment has actually gone out, which is a step beyond `recognized`.",
            "example": "pending"
          },
          "rewarded_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Null unless `status` is `rewarded`."
          },
          "reward_details": {
            "type": "string",
            "nullable": true,
            "description": "What was sent, when the tenant recorded it.",
            "example": "Engraved watch"
          },
          "can_recognize": {
            "type": "boolean",
            "description": "Whether to offer the Recognize / Celebrate action on THIS row — the tenant allows instant awards AND this milestone hasn't been awarded yet. Offering it on an already-recognized row invites a duplicate award, which is why the web hides it there too.\n\nThe action is `POST /recognitions/quick_award`, and it must carry `anniversary_years` set to this row's `years` — that is what records the recognition and flips this row to `recognized: true`.",
            "example": true
          }
        }
      },
      "RecognitionTeamHighlight": {
        "type": "object",
        "nullable": true,
        "description": "Null when the team has no recognition at all. `count` agrees with that person's matching column in `team_members`.",
        "properties": {
          "user": {
            "$ref": "#/components/schemas/RecognitionPerson"
          },
          "count": {
            "type": "integer",
            "example": 12
          }
        }
      },
      "RecognitionTeamActivityRow": {
        "type": "object",
        "description": "One recognition, normalized across formal awards and peer shout-outs so the list renders through one row shape.",
        "properties": {
          "recipient": {
            "$ref": "#/components/schemas/RecognitionPerson"
          },
          "group_recipients": {
            "type": "array",
            "nullable": true,
            "description": "Present only on a collapsed GROUP give — one recognition fanned out to several people arrives as ONE row naming all of them, because rendering each separately let a single ten-person shout-out fill the whole list.",
            "items": {
              "$ref": "#/components/schemas/RecognitionPerson"
            }
          },
          "recipient_label": {
            "type": "string",
            "description": "\"Alice, Bob and Carol\" for a collapsed group give, the single recipient's name otherwise — so no client-side name joining is needed.",
            "example": "Ada Lovelace and Bo Diaz"
          },
          "giver": {
            "allOf": [
              {
                "$ref": "#/components/schemas/RecognitionPerson"
              }
            ],
            "nullable": true,
            "description": "Null when `anonymous` is true — anonymity is enforced server-side, not merely hidden by the client."
          },
          "anonymous": {
            "type": "boolean",
            "example": false
          },
          "program": {
            "type": "string",
            "description": "The award program's name, or \"Peer Recognition\" for a shout-out.",
            "example": "Peer Recognition"
          },
          "message": {
            "type": "string",
            "nullable": true,
            "description": "The award title or the shout-out body."
          },
          "points": {
            "type": "integer",
            "description": "The recognition's value in reward points. 0 for a pointless shout-out.",
            "example": 250
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RecognitionTeamMemberRow": {
        "type": "object",
        "properties": {
          "user": {
            "$ref": "#/components/schemas/RecognitionPerson"
          },
          "job_title": {
            "type": "string",
            "nullable": true,
            "description": "Their own job title, falling back to their organizational role.",
            "example": "Support Specialist"
          },
          "received_count": {
            "type": "integer",
            "description": "All-time awards + shout-outs received, under the tenant-wide counting rules.",
            "example": 8
          },
          "given_count": {
            "type": "integer",
            "example": 3
          },
          "this_month_count": {
            "type": "integer",
            "description": "Received in the current CALENDAR month — always a subset of `received_count`.",
            "example": 2
          }
        }
      },
      "RecognitionProfileSubject": {
        "type": "object",
        "description": "The person whose recognition profile this is. Same shape as a `team_members` row from `/recognitions/team`, so the roster and the detail screen are one client model.",
        "properties": {
          "user": {
            "$ref": "#/components/schemas/RecognitionPerson"
          },
          "job_title": {
            "type": "string",
            "nullable": true,
            "description": "Their own job title, falling back to their organizational role in THIS business. Null when neither is set — the web renders the generic \"Team Member\" for that case.",
            "example": "Print Operator"
          }
        }
      },
      "RecognitionProfileMonthTile": {
        "type": "object",
        "description": "One \"This Month\" tile, over the current CALENDAR month. The total is a roll-up of BOTH recognition tables, matching the \"This Month\" column on the roster that links here; the split is published so a client can render the sub-line without a second request.",
        "properties": {
          "total": {
            "type": "integer",
            "description": "awards + shout_outs.",
            "example": 20
          },
          "awards": {
            "type": "integer",
            "example": 5
          },
          "shout_outs": {
            "type": "integer",
            "example": 15
          }
        }
      },
      "RecognitionProfileSection": {
        "type": "object",
        "description": "One of the profile's three lists. Not paginated — the latest `limit` rows, newest first, plus the full `total_count`, which together are what the web prints as \"Showing latest 20 of N\". Use `/recognitions/feed` for deeper history.",
        "properties": {
          "total_count": {
            "type": "integer",
            "description": "Every row that matches, before the limit — so a client can say \"20 of 47\" rather than implying the list is complete.",
            "example": 47
          },
          "showing": {
            "type": "integer",
            "description": "Rows actually in `items`.",
            "example": 20
          },
          "limit": {
            "type": "integer",
            "description": "The server's per-list cap.",
            "example": 20
          },
          "has_more": {
            "type": "boolean",
            "description": "`total_count` exceeds `limit`.",
            "example": true
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RecognitionProfileRow"
            }
          }
        }
      },
      "RecognitionProfileRow": {
        "type": "object",
        "description": "One recognition, normalized across formal awards and peer shout-outs so all three lists render through one row shape. This is the same identity half as `RecognitionFeedItem` (and the dashboard's activity rows) — the same server-side serializer — minus the browsable feed's own engagement, boost and permission blocks. Open the full detail with `GET /recognitions/awards/{id}` or `GET /recognitions/posts/{id}` using `type` to pick the path.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "award",
              "recognition_post"
            ],
            "description": "`award` is a formal program award; `recognition_post` is peer recognition. Also selects the detail path for this row."
          },
          "id": {
            "type": "integer",
            "example": 8814
          },
          "title": {
            "type": "string",
            "nullable": true,
            "description": "Award title. Null for peer posts, which lead with the message.",
            "example": "Excellence Award"
          },
          "message": {
            "type": "string",
            "nullable": true,
            "description": "The recognition text — an award's description, or a post's full content. The web truncates it at 150 characters; the API sends it whole so the client decides."
          },
          "points": {
            "type": "integer",
            "description": "Reward points the RECIPIENT earned. An award stores dollars and converts; a peer post stores points directly. Always present (0 when none) — the web hides the badge for 0.",
            "example": 250
          },
          "recipient": {
            "$ref": "#/components/schemas/RecognitionPerson"
          },
          "group_recipients": {
            "type": "array",
            "nullable": true,
            "description": "Present only on a collapsed GROUP give — every recipient of the one recognition. Null for an ordinary single-recipient row.",
            "items": {
              "$ref": "#/components/schemas/RecognitionPerson"
            }
          },
          "giver": {
            "allOf": [
              {
                "$ref": "#/components/schemas/RecognitionPerson"
              }
            ],
            "nullable": true,
            "description": "Null when `anonymous` is true — anonymity is enforced server-side, not merely hidden by the client. For an automated lifecycle award this is the \"System (Automated)\" pseudo-person with a null id; the system principal is never exposed. On the `awards_given` list this is the profile's subject."
          },
          "anonymous": {
            "type": "boolean",
            "description": "The giver chose to stay unattributed; `giver` is null.",
            "example": false
          },
          "program": {
            "type": "string",
            "nullable": true,
            "description": "The award program's name. Null for a peer shout-out.",
            "example": "Spot Recognition"
          },
          "category": {
            "type": "string",
            "nullable": true,
            "example": "Project Completion"
          },
          "company_value": {
            "type": "string",
            "nullable": true,
            "description": "The company value this recognition celebrates.",
            "example": "Teamwork"
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the recognition happened — an award's awarded_at, a post's published_at. Every list is ordered by this, newest first."
          }
        }
      },
      "RfpAnswerInput": {
        "type": "object",
        "properties": {
          "question": {
            "type": "string"
          },
          "answer": {
            "type": "string"
          },
          "category": {
            "type": "string",
            "enum": [
              "technical",
              "compliance",
              "commercial",
              "delivery",
              "other"
            ]
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "active": {
            "type": "boolean"
          }
        }
      },
      "RfpAnswer": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "question": {
            "type": "string"
          },
          "answer": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "active": {
            "type": "boolean"
          },
          "stale": {
            "type": "boolean",
            "description": "Not reviewed (or edited) within the workspace's staleness window (Settings > Answer freshness, 12 months by default)"
          },
          "usage_count": {
            "type": "integer"
          },
          "last_used_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "last_reviewed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "owner": {
            "$ref": "#/components/schemas/RfpPerson"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RfpPerson": {
        "type": "object",
        "nullable": true,
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "RfpResponseSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "on_hold",
              "in_review",
              "approved",
              "submitted",
              "declined"
            ]
          },
          "status_label": {
            "type": "string"
          },
          "locked": {
            "type": "boolean",
            "description": "Approved or submitted — section drafts refuse edits"
          },
          "due_on": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "responsible": {
            "$ref": "#/components/schemas/RfpPerson"
          },
          "section_count": {
            "type": "integer"
          },
          "open_gap_count": {
            "type": "integer"
          },
          "assessment_id": {
            "type": "integer",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RfpResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/RfpResponseSummary"
          },
          {
            "type": "object",
            "properties": {
              "notes": {
                "type": "string",
                "nullable": true
              },
              "internal_target_on": {
                "type": "string",
                "format": "date",
                "nullable": true
              },
              "approved_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "submitted_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "sections": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/RfpSection"
                }
              }
            }
          }
        ]
      },
      "RfpSection": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "position": {
            "type": "integer"
          },
          "kind": {
            "type": "string",
            "description": "requirement, or a narrative kind (cover letter, executive summary…)"
          },
          "requirement": {
            "type": "string"
          },
          "category": {
            "type": "string",
            "nullable": true
          },
          "mandatory": {
            "type": "boolean"
          },
          "draft": {
            "type": "string",
            "nullable": true
          },
          "grounded_on": {
            "type": "string",
            "nullable": true,
            "enum": [
              "library",
              "human",
              "capabilities",
              "knowledge",
              "none"
            ]
          },
          "grounding": {
            "type": "string",
            "description": "Human-readable provenance"
          },
          "needs_input": {
            "type": "boolean"
          },
          "reviewed": {
            "type": "boolean"
          },
          "owner": {
            "$ref": "#/components/schemas/RfpPerson"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RfpAssessmentSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "issuer": {
            "type": "string",
            "nullable": true
          },
          "recommendation": {
            "type": "string",
            "nullable": true,
            "enum": [
              "go",
              "conditional",
              "no_bid"
            ],
            "description": "null when the assessment ran without a capability profile or found no requirements"
          },
          "score": {
            "type": "integer",
            "nullable": true
          },
          "hard_gated": {
            "type": "boolean"
          },
          "terms_screened": {
            "type": "boolean",
            "nullable": true
          },
          "response_id": {
            "type": "integer",
            "nullable": true,
            "description": "The RFP response drafted from this assessment, if any"
          },
          "run_by": {
            "type": "string",
            "description": "Email of the person who ran it"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RfpAssessment": {
        "allOf": [
          {
            "$ref": "#/components/schemas/RfpAssessmentSummary"
          },
          {
            "type": "object",
            "properties": {
              "company_name": {
                "type": "string",
                "nullable": true
              },
              "rfp_meta": {
                "type": "object",
                "properties": {
                  "issuer": {
                    "type": "string",
                    "nullable": true
                  },
                  "estimated_value": {
                    "type": "string",
                    "nullable": true
                  },
                  "submission_deadline": {
                    "type": "string",
                    "nullable": true
                  }
                }
              },
              "no_profile": {
                "type": "boolean"
              },
              "no_requirements": {
                "type": "boolean"
              },
              "input_truncated": {
                "type": "boolean"
              },
              "breakdown": {
                "type": "array",
                "items": {
                  "type": "object"
                }
              },
              "requirements": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "text": {
                      "type": "string"
                    },
                    "category": {
                      "type": "string"
                    },
                    "mandatory": {
                      "type": "boolean"
                    },
                    "we_can_meet": {
                      "type": "string"
                    },
                    "evidence": {
                      "type": "string"
                    }
                  }
                }
              },
              "unmet_mandatory": {
                "type": "array",
                "items": {
                  "type": "object"
                }
              },
              "disqualifier_hits": {
                "type": "array",
                "items": {
                  "type": "object"
                }
              }
            }
          }
        ]
      },
      "MyTicketsWidgetResponse": {
        "type": "object",
        "required": [
          "success",
          "user_role",
          "tabs"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          },
          "user_role": {
            "type": "string",
            "enum": [
              "member",
              "agent",
              "manager",
              "admin",
              "super_admin"
            ],
            "description": "Effective role of the current user in this business"
          },
          "tabs": {
            "type": "array",
            "description": "Ordered array of applicable tabs (role-dependent)",
            "items": {
              "$ref": "#/components/schemas/TicketTab"
            }
          }
        }
      },
      "TicketTab": {
        "type": "object",
        "required": [
          "key",
          "label",
          "total_count",
          "tickets"
        ],
        "properties": {
          "key": {
            "type": "string",
            "enum": [
              "created_by_me",
              "assigned_to_me",
              "pending_approval_for_me"
            ],
            "description": "Machine-readable tab identifier"
          },
          "label": {
            "type": "string",
            "description": "Human-readable tab label for display",
            "example": "Created by Me"
          },
          "total_count": {
            "type": "integer",
            "description": "Total number of tickets in this tab (for badge/View All)",
            "example": 24
          },
          "tickets": {
            "type": "array",
            "description": "Preview tickets (at most per_tab items)",
            "items": {
              "$ref": "#/components/schemas/SupportTicketSummary"
            }
          }
        }
      },
      "AbsenceReason": {
        "type": "object",
        "description": "One tenant-configured absence reason code, as offered by\n`GET /absence_reasons`. Post `id` back as\n`absence_report[absence_reason_code_id]` when filing the report.\n",
        "required": [
          "id",
          "code",
          "name",
          "label",
          "active"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "code": {
            "type": "string",
            "description": "Stable per-tenant identifier (e.g. `sick`, `running_late`). Renaming the reason changes `name`/`label`, never this.\n"
          },
          "name": {
            "type": "string",
            "description": "The tenant's own label — the exact string the web picker shows."
          },
          "label": {
            "type": "string",
            "description": "Alias of `name`. Both carry the same tenant string."
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "category": {
            "type": "string",
            "enum": [
              "general",
              "illness",
              "personal",
              "transportation",
              "family",
              "other"
            ]
          },
          "category_label": {
            "type": "string",
            "nullable": true,
            "description": "Display form of `category` (e.g. `illness` -> \"Illness/Medical\")."
          },
          "requires_documentation": {
            "type": "boolean",
            "description": "ADVISORY ONLY. Nothing enforces it — no absence surface has an\nattachment field — so render it as guidance (\"documentation may be\nrequired\"). Do not gate submission on it, and do not promise an\nupload step. The web form deliberately stopped appending\n\"(Requires Documentation)\" to the option text for this reason.\n"
          },
          "documentation_threshold_days": {
            "type": "integer",
            "description": "Absence length past which the tenant expects documentation. 0 = none."
          },
          "requires_manager_approval": {
            "type": "boolean"
          },
          "advance_notice_required_days": {
            "type": "integer",
            "description": "Notice the tenant's policy expects for this reason. 0 = none."
          },
          "requires_estimated_arrival": {
            "type": "boolean",
            "description": "Collect an estimated arrival time for this reason — it is a DELAY, not an absence. True for the code the server treats as \"running late\", which is also what decides whether the ETA is shown to the manager, so key the field on this rather than on the label.\n"
          },
          "is_paid": {
            "type": "boolean"
          },
          "active": {
            "type": "boolean",
            "description": "False only in an `include_inactive=true` response — a retired code, kept so an existing report can still be named. Never offer one in a new-report picker.\n"
          }
        }
      },
      "UndoClockOutWindow": {
        "type": "object",
        "description": "Server-truth state of the Undo Clock-Out window, as carried by\n`POST /attendance_records/{id}/check_out`,\n`POST /attendance_records/{id}/undo_clock_out` and\n`GET /attendance_records/status`. The same four keys are always present;\nwhen the undo is unavailable, `can_undo_clock_out` is false,\n`undo_deadline` is null and `undo_seconds_remaining` is 0.\n",
        "required": [
          "attendance_record_id",
          "can_undo_clock_out",
          "undo_deadline",
          "undo_seconds_remaining"
        ],
        "properties": {
          "attendance_record_id": {
            "type": "integer",
            "nullable": true,
            "description": "The record to POST the undo to, or null when there is nothing to undo."
          },
          "can_undo_clock_out": {
            "type": "boolean",
            "description": "Whether the caller may undo this clock-out right now."
          },
          "undo_deadline": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "ISO-8601 instant the window closes (clock-out time + 15 minutes)."
          },
          "undo_seconds_remaining": {
            "type": "integer",
            "description": "Whole seconds left, measured on the server clock. Render the\ncountdown from this rather than from `undo_deadline` so a device\nwith clock drift still counts down correctly.\n"
          }
        }
      },
      "SocialPostEnvelope": {
        "type": "object",
        "properties": {
          "post": {
            "type": "object",
            "properties": {
              "urn": {
                "type": "string",
                "example": "urn:li:share:1234567890"
              },
              "status": {
                "type": "string",
                "enum": [
                  "pending",
                  "published",
                  "scheduled",
                  "failed"
                ]
              },
              "author": {
                "type": "string",
                "enum": [
                  "organization",
                  "member"
                ]
              },
              "provider": {
                "type": "string",
                "enum": [
                  "linkedin"
                ]
              },
              "published_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "error_code": {
                "type": "string",
                "nullable": true,
                "description": "Internal error vocabulary — set when status=failed.\nOne of: not_connected, auth_expired, invalid_payload,\nrate_limited, provider_error, unexpected.\n"
              },
              "error_message": {
                "type": "string",
                "nullable": true
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        }
      },
      "ErrorEnvelope": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              },
              "details": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        }
      },
      "SurveySummary": {
        "type": "object",
        "description": "A survey as it appears in a list. The serializer is an explicit allowlist — the record is never rendered wholesale, so no internal column and no respondent identity can leak into a response.",
        "required": [
          "id",
          "name",
          "survey_type",
          "status",
          "is_open",
          "is_anonymous",
          "questions_count"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "survey_type": {
            "type": "string",
            "enum": [
              "engagement",
              "pulse",
              "custom",
              "manager_feedback"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "active",
              "closed",
              "archived"
            ]
          },
          "is_open": {
            "type": "boolean",
            "description": "True when the survey is `active` AND the current time is inside its `opens_at`/`closes_at` window. Not a restatement of `status` — an `active` survey outside its window reports `is_open: false`."
          },
          "is_anonymous": {
            "type": "boolean",
            "description": "When true, responses are stored with identity hidden on every display surface, and results below the tenant's anonymity floor are suppressed entirely."
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "opens_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "closes_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "questions_count": {
            "type": "integer",
            "description": "Caller-fillable input questions only; display blocks (instruction / section / header) are excluded. `0` for a survey with no form template."
          },
          "response_count": {
            "type": "integer",
            "nullable": true,
            "description": "PRESENT ONLY for a caller who may see it, and it means different things by tier. In the `scope=created` list, and on the detail payload at the `:all` / `:aggregate` results tiers, it is the whole-survey count of submitted-or-approved responses. On the detail payload at the `:team` tier it counts the caller's own direct reports only, and is `null` when an anonymous survey's team count is below the tenant's anonymity floor. Absent entirely from the employee self-service list and for a caller at the `:none` tier."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SurveyDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SurveySummary"
          },
          {
            "type": "object",
            "description": "The single-survey payload — everything above plus the questions.",
            "required": [
              "completed",
              "questions"
            ],
            "properties": {
              "completed": {
                "type": "boolean",
                "description": "Whether this caller has already submitted a response. A question of FACT, not of policy — a survey whose template allows multiple submissions still accepts another response while this is true."
              },
              "questions": {
                "type": "array",
                "description": "The caller-fillable questions, in builder order.",
                "items": {
                  "$ref": "#/components/schemas/SurveyQuestion"
                }
              },
              "target_audience_count": {
                "type": "integer",
                "description": "Whole-survey audience size. Present only at the `:all` / `:aggregate` results tiers — never for a `:team` manager, and never for a caller at the `:none` tier."
              },
              "completion_rate": {
                "type": "number",
                "format": "float",
                "description": "Whole-survey completion as a PERCENTAGE (0–100, one decimal place), not a fraction. Same tier rule as `target_audience_count`.",
                "example": 42.9
              }
            }
          }
        ]
      },
      "SurveyQuestion": {
        "type": "object",
        "description": "One caller-fillable question.",
        "required": [
          "id",
          "field_name",
          "label",
          "field_type",
          "required"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "field_name": {
            "type": "string",
            "description": "The key to use for this question inside `submission_data`."
          },
          "label": {
            "type": "string"
          },
          "field_type": {
            "type": "string",
            "description": "The underlying form field type — e.g. `text`, `textarea`, `number`, `email`, `select`, `multiselect`, `checkbox`, `radio`, `date`, `rating`, `slider`, `scale`, `matrix`, `rich_text`, `signature`, `file`, `image`. Determines the answer shape accepted by `respond` and whether the question is rolled up in `results`."
          },
          "required": {
            "type": "boolean"
          },
          "help_text": {
            "type": "string",
            "nullable": true,
            "description": "Null when the question carries no help text."
          },
          "options": {
            "type": "array",
            "description": "The selectable option values, for choice-style questions. An empty array for every other field type. Option objects configured with separate value/label pairs are flattened to their value.",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "SurveySubmission": {
        "type": "object",
        "description": "The receipt returned when a response is accepted.",
        "required": [
          "id",
          "survey_id",
          "status",
          "is_anonymous"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "survey_id": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "description": "Always `submitted` on this path — this endpoint writes that status explicitly and nothing downstream reassigns it.",
            "example": "submitted"
          },
          "is_anonymous": {
            "type": "boolean",
            "description": "The SURVEY's anonymity setting, echoed back on the receipt."
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time"
          },
          "message": {
            "type": "string",
            "description": "A ready-to-display confirmation naming the survey.",
            "example": "Thank you! Your response to 'Q3 Engagement Pulse' has been submitted."
          }
        }
      },
      "SurveyResults": {
        "type": "object",
        "description": "Aggregate results for the caller's tier. Raw per-respondent rows and respondent identity never appear here.",
        "required": [
          "survey",
          "scope",
          "minimum_response_threshold",
          "threshold_met",
          "question_summaries"
        ],
        "properties": {
          "survey": {
            "$ref": "#/components/schemas/SurveyResultsSurvey"
          },
          "scope": {
            "type": "string",
            "enum": [
              "full",
              "team"
            ],
            "description": "`full` for the `:all` and `:aggregate` tiers (org-wide numbers); `team` for a manager reading a slice narrowed to their own direct reports."
          },
          "minimum_response_threshold": {
            "type": "integer",
            "minimum": 5,
            "description": "The tenant's anonymity floor, after the platform minimum of 5 is applied. A tenant may raise this but not lower it."
          },
          "threshold_met": {
            "type": "boolean",
            "description": "Whether the response count for this scope is positive AND at or above the threshold. Always `false` in the suppressed branch."
          },
          "anonymity_protected": {
            "type": "boolean",
            "description": "PRESENT ONLY when suppression fired — an anonymous survey whose response count for this scope is below the threshold. When present it is `true`, `question_summaries` is empty, and no aggregate of any kind is returned. Absent (not `false`) on an unsuppressed response.",
            "example": true
          },
          "question_summaries": {
            "type": "array",
            "description": "One entry per rolled-up question, in builder order. ONLY these field types are rolled up — `rating`, `number`, `slider`, `scale` (numeric); `select`, `radio`, `checkbox`, `multiselect` (choice); `text`, `textarea` (text). Every other question type is omitted from this array entirely. Empty in the suppressed branch.",
            "items": {
              "$ref": "#/components/schemas/SurveyQuestionSummary"
            }
          }
        }
      },
      "SurveyResultsSurvey": {
        "type": "object",
        "description": "The survey header block inside a results payload.",
        "required": [
          "id",
          "name",
          "is_anonymous",
          "status"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "is_anonymous": {
            "type": "boolean"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "active",
              "closed",
              "archived"
            ]
          },
          "response_count": {
            "type": "integer",
            "nullable": true,
            "description": "Responses counted for this scope — org-wide for `full`, the caller's direct reports for `team`. `null` when a `team`-scoped count on an anonymous survey falls below the anonymity floor."
          },
          "target_audience_count": {
            "type": "integer",
            "description": "`full` scope only — a team slice has no audience denominator."
          },
          "completion_rate": {
            "type": "number",
            "format": "float",
            "description": "`full` scope only. A PERCENTAGE (0–100, one decimal place), computed from the same `response_count` above; `0.0` when the audience is empty."
          }
        }
      },
      "SurveyQuestionSummary": {
        "type": "object",
        "description": "One question's roll-up. `type` selects which of the remaining fields are present, and any of the three can instead carry `insufficient_data`.",
        "required": [
          "field_name",
          "label",
          "type",
          "count"
        ],
        "properties": {
          "field_name": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "numeric",
              "choice",
              "text"
            ]
          },
          "count": {
            "type": "integer",
            "description": "For `numeric`, the number of answers that parsed as a number. For `choice`, the number of respondents who chose at least one option. For `text`, the number of non-blank answers."
          },
          "insufficient_data": {
            "type": "boolean",
            "description": "`numeric` and `choice` only, and present only when `count` is below `minimum_response_threshold`. When present, `average`/`min`/`max` and `distribution` are all absent. `text` summaries are never marked this way — they carry a count and nothing else at any volume.",
            "example": true
          },
          "average": {
            "type": "number",
            "format": "float",
            "description": "`numeric` only, rounded to 2 decimal places."
          },
          "min": {
            "type": "number",
            "format": "float",
            "description": "`numeric` only."
          },
          "max": {
            "type": "number",
            "format": "float",
            "description": "`numeric` only."
          },
          "distribution": {
            "type": "object",
            "description": "`choice` only — keyed by the stored answer value. `percentage` is that option's share of TOTAL SELECTIONS, not of respondents, so on a multi-select question the percentages are over the number of boxes ticked rather than the number of people; `0.0` when nothing was selected.",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "count": {
                  "type": "integer"
                },
                "percentage": {
                  "type": "number",
                  "format": "float"
                }
              }
            }
          }
        }
      },
      "SurveyAnonymousFeedback": {
        "type": "object",
        "description": "A recorded anonymous feedback item. No submitter identity is stored on it.",
        "required": [
          "id",
          "topic",
          "topic_label",
          "message",
          "status"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "topic": {
            "type": "string",
            "enum": [
              "workload",
              "safety",
              "leadership",
              "culture",
              "compensation",
              "communication",
              "other"
            ]
          },
          "topic_label": {
            "type": "string",
            "description": "The display label for `topic`, from the app's single label taxonomy (e.g. `workload` renders as \"Workload & Work-Life Balance\"). An unmapped topic falls back to a titleized form.",
            "example": "Workload & Work-Life Balance"
          },
          "message": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "new",
              "in_progress",
              "closed"
            ],
            "description": "Always `new` on creation."
          },
          "claim_code": {
            "type": "string",
            "nullable": true,
            "description": "The one-way code the submitter keeps in order to look up this item's status later. Never tied to identity. 8 characters from an unambiguous uppercase alphabet (0/O/1/I/L excluded so a hand-copied code cannot be misread), unique per business.",
            "example": "K7QM3XPB"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SurveysPaginationMeta": {
        "type": "object",
        "description": "Pagination metadata, computed off the live relation rather than off the returned page. The same four values are also returned as the `X-Total-Count`, `X-Total-Pages`, `X-Current-Page` and `X-Per-Page` response headers.",
        "required": [
          "total_count",
          "total_pages",
          "current_page",
          "per_page"
        ],
        "properties": {
          "total_count": {
            "type": "integer"
          },
          "total_pages": {
            "type": "integer"
          },
          "current_page": {
            "type": "integer"
          },
          "per_page": {
            "type": "integer"
          },
          "has_next_page": {
            "type": "boolean"
          },
          "has_prev_page": {
            "type": "boolean"
          }
        }
      },
      "UnreadNotificationCountRef": {
        "type": "integer",
        "description": "Platform piggyback field — the calling user's unread, active notification count in the current business, used for the native app badge. Present only on the endpoint that renders through the piggyback path (`GET /surveys/surveys`), only when both a user and a business resolved, and degrades to `0` rather than erroring.",
        "example": 3
      },
      "PlatformHttpMeta": {
        "type": "object",
        "description": "Platform HTTP-enhancement block added by the piggyback renderer. Present on `GET /surveys/surveys` only. Its `http.caching.etag` is what a client echoes in `If-None-Match` to get a `304`.",
        "properties": {
          "http": {
            "type": "object",
            "properties": {
              "caching": {
                "type": "object"
              },
              "navigation": {
                "type": "object"
              },
              "performance": {
                "type": "object"
              },
              "client_hints": {
                "type": "object"
              }
            }
          }
        }
      },
      "TrainingQuizEnrollmentState": {
        "type": "object",
        "nullable": true,
        "description": "The caller's standing in the COURSE the quiz belongs to. Every quiz endpoint requires an enrollment, so this is null only in the degenerate case where one has been removed between requests.\n\nThe SCORM card endpoints report the same block from the same fields, and reference this schema rather than restating it.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 5512
          },
          "status": {
            "type": "string",
            "enum": [
              "enrolled",
              "in_progress",
              "completed",
              "cancelled"
            ],
            "description": "**This is the field that routes a client.** Show a course-completion screen for `completed` and nothing else. Note a learner can sit at `progress_percentage: 100` and still be `in_progress` — passing a required quiz is a separate condition from finishing the lessons, so do not treat 100% as completion."
          },
          "progress_percentage": {
            "type": "integer",
            "example": 83
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "TrainingQuizCourseState": {
        "type": "object",
        "description": "Where the learner now stands in the course — returned by submit and results, because passing an assessment can complete the course and a client needs to know that without a second request.",
        "properties": {
          "enrollment": {
            "$ref": "#/components/schemas/TrainingQuizEnrollmentState"
          },
          "certificate": {
            "type": "object",
            "description": "A STATE, not just an id, because issuing a certificate is asynchronous. A client that has just submitted a passing final attempt will legitimately see `pending`; poll `GET /training/my_records/certificates` rather than reading a missing id as \"this course has no certificate\".",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "issued",
                  "pending",
                  "not_applicable"
                ],
                "description": "`not_applicable` while the enrollment is not yet complete; `pending` once it is but the certificate job has not finished; `issued` when `id` is present."
              },
              "id": {
                "type": "integer",
                "nullable": true,
                "example": 8801
              }
            }
          }
        }
      },
      "TrainingQuizCard": {
        "type": "object",
        "description": "The assessment entry point — see `GET .../lessons/{lesson_id}/quiz`.",
        "properties": {
          "lesson_id": {
            "type": "integer",
            "nullable": true,
            "example": 412
          },
          "lesson_title": {
            "type": "string",
            "nullable": true,
            "example": "Safety Assessment"
          },
          "quiz": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "example": 77
              },
              "title": {
                "type": "string",
                "example": "Safety Assessment"
              },
              "description": {
                "type": "string",
                "nullable": true
              },
              "instructions": {
                "type": "string",
                "nullable": true,
                "description": "Authored pre-flight copy (\"you may not pause this\", \"have your SDS sheet to hand\"). RENDER IT before the learner starts — it is instructions they were meant to read first, not decoration."
              },
              "question_count": {
                "type": "integer",
                "example": 7
              },
              "total_points": {
                "type": "integer",
                "example": 12
              },
              "passing_score": {
                "type": "integer",
                "description": "Percent needed to pass.",
                "example": 80
              },
              "time_limit_minutes": {
                "type": "integer",
                "nullable": true,
                "description": "Null on an untimed quiz — draw no timer at all rather than a zero.",
                "example": 15
              },
              "question_display": {
                "type": "string",
                "enum": [
                  "one_per_page",
                  "all_at_once"
                ],
                "description": "How the quiz was authored to be taken: a stepper, or every question on one page. Both post the identical `answers` payload."
              },
              "shuffled": {
                "type": "boolean",
                "description": "Whether question ORDER is shuffled per learner."
              }
            }
          },
          "attempts": {
            "type": "object",
            "properties": {
              "taken": {
                "type": "integer",
                "description": "TERMINAL attempts used (submitted or timed out). An attempt still open is the learner's current one, not a used one.",
                "example": 1
              },
              "max": {
                "type": "integer",
                "nullable": true,
                "description": "Null when unlimited.",
                "example": 3
              },
              "remaining": {
                "type": "integer",
                "nullable": true,
                "description": "**Null, not 0, when there is no attempt limit** — an unlimited pool has no remainder, and 0 would read as exhausted.",
                "example": 2
              },
              "can_attempt": {
                "type": "boolean",
                "example": true
              },
              "block_reason": {
                "type": "string",
                "nullable": true,
                "description": "Null when nothing blocks. Otherwise a learner-facing sentence naming which of the three gates stopped them: the pool is used up, they have already passed and the quiz only allows retakes after a fail, or a cooldown is running. Display it verbatim.",
                "example": "You've used all your attempts for this quiz."
              },
              "next_available_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When a cooldown lifts, so a client can count down instead of polling to discover the gate has gone. Null when no cooldown is running."
              }
            }
          },
          "passed": {
            "type": "boolean",
            "description": "Whether ANY attempt in THIS enrollment has passed — scoped to the enrollment, never the learner's lifetime, so a re-assigned course is not pre-passed by last year's attempt."
          },
          "outcome": {
            "type": "object",
            "nullable": true,
            "description": "The learner's standing result, or null before anything is graded. This is the attempt their Pass/Fail VERDICT came from — **not** necessarily the most recent one. On a `highest` score policy a failing retake leaves the verdict here on the earlier pass, so do not infer it from the newest attempt.",
            "properties": {
              "attempt_id": {
                "type": "integer",
                "example": 9014
              },
              "attempt_number": {
                "type": "integer",
                "example": 1
              },
              "score_percentage": {
                "type": "integer",
                "nullable": true,
                "example": 86
              },
              "passed": {
                "type": "boolean"
              },
              "timed_out": {
                "type": "boolean"
              },
              "submitted_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "effective_score": {
                "type": "integer",
                "nullable": true,
                "description": "The score that COUNTS toward completion and the transcript, under the quiz's score policy (highest / latest / average / first). Can differ from `score_percentage` above.",
                "example": 86
              }
            }
          },
          "open_attempt": {
            "type": "object",
            "nullable": true,
            "description": "An attempt still in progress, or null. Its clock has been running since `started_at` and has NOT paused.\n\nAn attempt can be still-open and already out of time, in which case it cannot be resumed — see `expired`. You no longer have to special-case that yourself: `cta.action` reports `view_results` for it, so following the CTA is correct on its own. `expired` is here so you can say WHY (\"your previous attempt ran out of time\") rather than to decide what the button does.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 9014
              },
              "attempt_number": {
                "type": "integer",
                "example": 2
              },
              "started_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "current_position": {
                "type": "integer",
                "nullable": true,
                "example": 3
              },
              "answered_count": {
                "type": "integer",
                "example": 3
              },
              "time_remaining_seconds": {
                "type": "integer",
                "nullable": true,
                "example": 742
              },
              "expired": {
                "type": "boolean",
                "description": "True when this attempt is still `in_progress` but its clock has run out — the learner walked away and the timer did not pause. **It is NOT resumable.** `POST .../quiz/attempts` will grade it from its saved answers and answer 409 `attempt_timed_out` with its id rather than returning a playable quiz, so label the control \"View result\" and go straight to that attempt's results.\n\nThe card is a read and deliberately does not finalize the attempt itself, which is why it reports the fact instead. Always false on an untimed quiz.",
                "example": false
              }
            }
          },
          "cta": {
            "type": "object",
            "description": "The ONE primary action, resolved server-side so every client offers the same thing. See the endpoint description for the table of actions.",
            "properties": {
              "action": {
                "type": "string",
                "enum": [
                  "start",
                  "resume",
                  "retake",
                  "view_results",
                  "locked"
                ]
              },
              "attempt_id": {
                "type": "integer",
                "nullable": true,
                "description": "The attempt to resume, retake from, or show results for."
              },
              "reason": {
                "type": "string",
                "nullable": true,
                "description": "Present on `locked` and `view_results` — why no further attempt is allowed."
              }
            }
          }
        }
      },
      "TrainingQuizQuestion": {
        "type": "object",
        "description": "ONE question as the LIVE PLAYER sees it. **Contains no correct answer, in any form** — read the answer-stripping contract at the top of this section, then switch on `question_type` and read the ONE type-specific block that applies:\n\n`multiple_choice` / `multiple_select` → `options`\n\n`true_false` → nothing (render the two)\n\n`text` → `text_input`\n\n`ranking` → `items` (already shuffled; render in the order given)\n\n`matching_text` / `matching_image` → `pairs` + `choices` (tokens)\n\n`hotspot` → `hotspot` (no regions)",
        "properties": {
          "id": {
            "type": "integer",
            "description": "The SEALED EDITION's question id. **Key your `answers` object by this** — not by an id from the course detail or a previous attempt.",
            "example": 30455
          },
          "position": {
            "type": "integer",
            "description": "1-based",
            "for \"Q3 of 7\". example": 3
          },
          "question_type": {
            "type": "string",
            "enum": [
              "multiple_choice",
              "true_false",
              "multiple_select",
              "text",
              "ranking",
              "matching_text",
              "matching_image",
              "hotspot"
            ]
          },
          "type_label": {
            "type": "string",
            "description": "Learner-facing name of the type, e.g. `Multiple choice`."
          },
          "question_text": {
            "type": "string"
          },
          "points": {
            "type": "integer",
            "example": 2
          },
          "mandatory": {
            "type": "boolean",
            "description": "Must be answered before the attempt can be submitted. The server enforces this too (422 `unanswered_mandatory`), so a client gate is a courtesy, not the rule."
          },
          "auto_graded": {
            "type": "boolean",
            "description": "Whether the SERVER grades it. False only for `text`, which a human reviews — surface that (\"reviewed by your instructor\") rather than implying an instant score."
          },
          "answered": {
            "type": "boolean",
            "description": "Whether `answer` counts as a real answer, judged per type (a `true_false` of `false` counts; a `matching_*` needs one filled pair). The same check the mandatory gate on submit uses."
          },
          "answer": {
            "nullable": true,
            "description": "What the learner has already entered, **in the same form you post it**, so a resumed attempt rehydrates. Per type: an option id string (`multiple_choice`), `\"true\"`/`\"false\"`, an array of option ids (`multiple_select`), a string (`text`), an ordered array of item ids (`ranking`), `{ \"<pair id>\": \"<token>\" }` (`matching_*` — TOKENS, not the ids stored internally), or an array of `{x, y}` percent points (`hotspot`). Null before anything is entered."
          },
          "options": {
            "type": "array",
            "description": "`multiple_choice` and `multiple_select` only. Nothing marks the correct one.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "example": "o2"
                },
                "text": {
                  "type": "string"
                }
              }
            }
          },
          "text_input": {
            "type": "object",
            "description": "`text` only — how to draw the box and what length to accept.",
            "properties": {
              "min_length": {
                "type": "integer",
                "nullable": true,
                "example": 20
              },
              "max_length": {
                "type": "integer",
                "nullable": true,
                "example": 500
              },
              "mode": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "short",
                  "long"
                ],
                "description": "Null for a question authored before the setting existed — pick your own default."
              }
            }
          },
          "items": {
            "type": "array",
            "description": "`ranking` only. **Already in the order to display**, and that order is a derangement of the authored (correct) one — no item sits in its correct slot. It is stable for the attempt, and becomes the learner's own arrangement once they have made one. Never sort it. Post the ids back in the order the learner leaves them.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "example": "s3"
                },
                "text": {
                  "type": "string"
                }
              }
            }
          },
          "pairs": {
            "type": "array",
            "description": "`matching_text` / `matching_image` only — the LEFT column, the fixed prompts, in authored order. `image_url` is present for `matching_image` (a 250x150 crop, so every tile is the same shape) and absent otherwise.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "example": "l2"
                },
                "left": {
                  "type": "string"
                },
                "image_url": {
                  "type": "string",
                  "nullable": true
                }
              }
            }
          },
          "choices": {
            "type": "array",
            "description": "`matching_text` / `matching_image` only — the RIGHT column, shuffled, and addressed **only** by opaque `token`. There is deliberately no id here to correlate with `pairs[].id`, because that correlation is the answer. Post `{ \"<pairs[].id>\": \"<choices[].token>\" }`; the server maps the token back. Tokens are stable for the question, so an autosaved answer round-trips.",
            "items": {
              "type": "object",
              "properties": {
                "token": {
                  "type": "string",
                  "example": "4f2c91ab77e30d15"
                },
                "text": {
                  "type": "string"
                }
              }
            }
          },
          "hotspot": {
            "type": "object",
            "description": "`hotspot` only. **The correct regions are absent entirely** — not shuffled, not partial. Post the learner's taps as an array of `{x, y}` percentages of the image (0-100, origin top-left) and the server decides correctness by point-in-region.",
            "properties": {
              "image_url": {
                "type": "string",
                "nullable": true,
                "description": "The background to tap on. Null if the admin never uploaded one."
              },
              "max_markers": {
                "type": "integer",
                "description": "How many markers the learner may place (the number of regions authored).",
                "example": 1
              },
              "rule": {
                "type": "string",
                "enum": [
                  "any_one",
                  "all"
                ],
                "description": "What the count MEANS, and it changes the task: `any_one` needs one region hit, `all` needs every region covered. Say which in the prompt — otherwise the server can fail a learner over a distinction the screen never mentioned."
              }
            }
          }
        }
      },
      "TrainingQuizAttempt": {
        "type": "object",
        "description": "A LIVE attempt and everything needed to render the player.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 9014
          },
          "attempt_number": {
            "type": "integer",
            "example": 2
          },
          "state": {
            "type": "string",
            "enum": [
              "in_progress",
              "submitted",
              "timed_out"
            ],
            "description": "Always `in_progress` from the player endpoint, which refuses the other two."
          },
          "started_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "The absolute instant the clock runs out. **Count down against this**, not by decrementing `time_remaining_seconds` locally — a device that slept will drift, and the server does not accept a late submission on trust. Null on an untimed quiz."
          },
          "time_remaining_seconds": {
            "type": "integer",
            "nullable": true,
            "description": "Seconds left as of this response; what to show immediately. Null on an untimed quiz.",
            "example": 742
          },
          "current_position": {
            "type": "integer",
            "nullable": true,
            "description": "The question the learner was last on, written by autosave — so a resume reopens there rather than back at question 1.",
            "example": 3
          },
          "question_count": {
            "type": "integer",
            "example": 7
          },
          "answered_count": {
            "type": "integer",
            "example": 3
          },
          "quiz": {
            "type": "object",
            "description": "The EFFECTIVE settings — frozen with this attempt's edition, not read live. An admin changing the passing score or time limit mid-attempt cannot move this learner's bar or clock, so these are the figures to display.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 77
              },
              "title": {
                "type": "string",
                "example": "Safety Assessment"
              },
              "passing_score": {
                "type": "integer",
                "example": 80
              },
              "time_limit_minutes": {
                "type": "integer",
                "nullable": true,
                "example": 15
              },
              "question_display": {
                "type": "string",
                "enum": [
                  "one_per_page",
                  "all_at_once"
                ]
              },
              "total_points": {
                "type": "integer",
                "example": 12
              },
              "attempt_number": {
                "type": "integer",
                "example": 2
              },
              "max_attempts": {
                "type": "integer",
                "nullable": true,
                "example": 3
              }
            }
          },
          "questions": {
            "type": "array",
            "description": "Every question, in the order frozen onto this attempt when it started. Render them in this order; it does not change across re-reads.",
            "items": {
              "$ref": "#/components/schemas/TrainingQuizQuestion"
            }
          }
        }
      },
      "TrainingQuizAnswers": {
        "type": "object",
        "description": "The learner's answers, keyed by the question ids **this attempt's player returned** (the sealed edition's ids — see the section header). Ids from anywhere else will be reported as unanswered.\n\nOne value shape per question type:\n\n`multiple_choice` → the option id, as a string (`\"o2\"`)\n\n`true_false` → `\"true\"` or `\"false\"`\n\n`multiple_select` → array of option ids (`[\"h1\",\"h2\"]`)\n\n`text` → the answer string\n\n`ranking` → array of item ids in the learner's order (`[\"s2\",\"s1\",\"s4\",\"s3\"]`)\n\n`matching_text` / `matching_image` → `{ \"<pair id>\": \"<choice token>\" }`\n\n`hotspot` → array of `{x, y}` percent points. May also be sent as a JSON STRING of that array, which is what the web player posts; both are accepted.\n\nSend the whole object on every autosave — it REPLACES what was stored, it is not merged.",
        "additionalProperties": true,
        "example": {
          "30455": "o2",
          "30456": [
            "h1",
            "h2"
          ],
          "30457": "Because stored energy can start the machine unexpectedly.",
          "30458": [
            "s2",
            "s1",
            "s4",
            "s3"
          ],
          "30459": {
            "l1": "4f2c91ab77e30d15",
            "l2": "9b70ee2c1d448a03"
          },
          "30460": [
            {
              "x": 42.5,
              "y": 63.1
            }
          ]
        }
      },
      "TrainingQuizAnswerReview": {
        "type": "object",
        "description": "One row of the post-submit review. The only place `correct_answer` and `explanation` are ever emitted.",
        "properties": {
          "question_id": {
            "type": "integer",
            "example": 30455
          },
          "position": {
            "type": "integer",
            "example": 1
          },
          "question_type": {
            "type": "string",
            "enum": [
              "multiple_choice",
              "true_false",
              "multiple_select",
              "text",
              "ranking",
              "matching_text",
              "matching_image",
              "hotspot"
            ]
          },
          "type_label": {
            "type": "string",
            "example": "Multiple choice"
          },
          "question_text": {
            "type": "string"
          },
          "points": {
            "type": "integer",
            "example": 1
          },
          "points_earned": {
            "type": "integer",
            "example": 1
          },
          "status": {
            "type": "string",
            "enum": [
              "correct",
              "incorrect",
              "pending_review"
            ],
            "description": "**`pending_review` is not a failure.** A `text` answer is graded by a human and is excluded from both sides of the score, so render it as awaiting review — showing it as incorrect tells the learner they failed a question nobody has read yet."
          },
          "your_answer": {
            "type": "string",
            "description": "The learner's answer, already rendered for display per type — `\"A → B → C\"` for a ranking, `\"3 of 4 pairs correct\"` for a matching, `\"2 areas marked\"` for a hotspot, and the literal `\"No answer\"` when they left it blank (so a row never shows an empty space the learner has to interpret)."
          },
          "correct_answer": {
            "type": "string",
            "nullable": true,
            "description": "The expected answer, rendered per type. **Null for `text`** — there is no single correct answer to show."
          },
          "explanation": {
            "type": "string",
            "nullable": true,
            "description": "The author's note on why, when one was written."
          }
        }
      },
      "TrainingQuizResult": {
        "type": "object",
        "description": "A GRADED attempt — the post-submit results screen.",
        "properties": {
          "attempt": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "example": 9014
              },
              "attempt_number": {
                "type": "integer",
                "example": 1
              },
              "state": {
                "type": "string",
                "enum": [
                  "submitted",
                  "timed_out"
                ]
              },
              "timed_out": {
                "type": "boolean",
                "description": "A persisted OUTCOME, not a clock reading: the attempt was finalized because time ran out, and was graded from the last autosave. Say \"time's up\" rather than presenting it as a normal submission."
              },
              "submitted_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "time_taken_seconds": {
                "type": "integer",
                "nullable": true,
                "example": 421
              },
              "formatted_time_taken": {
                "type": "string",
                "example": "7m 1s"
              }
            }
          },
          "score": {
            "type": "object",
            "description": "**The three counts are separate on purpose.** Written (`text`) answers are human-graded and excluded from both the numerator and the denominator, so compose any \"N of M correct\" sentence from `correct_count` / `auto_graded_count` and report `pending_review_count` alongside. A single fused figure would count a question nobody has graded and contradict the review list below it.",
            "properties": {
              "score_percentage": {
                "type": "integer",
                "nullable": true,
                "example": 86
              },
              "passed": {
                "type": "boolean"
              },
              "points_earned": {
                "type": "integer",
                "example": 10
              },
              "total_points": {
                "type": "integer",
                "description": "Auto-gradable points on offer — the denominator of `points_earned`.",
                "example": 12
              },
              "passing_score": {
                "type": "integer",
                "description": "The bar this attempt was actually graded against, frozen with its edition — print this even if the quiz has since moved.",
                "example": 80
              },
              "correct_count": {
                "type": "integer",
                "example": 6
              },
              "incorrect_count": {
                "type": "integer",
                "example": 0
              },
              "auto_graded_count": {
                "type": "integer",
                "description": "The denominator of correct/incorrect — auto-gradable questions only.",
                "example": 6
              },
              "pending_review_count": {
                "type": "integer",
                "description": "Written answers a human has still to read. Non-zero means the score is provisional for those rows.",
                "example": 1
              },
              "effective_score": {
                "type": "integer",
                "nullable": true,
                "description": "The score that COUNTS toward completion and the transcript, per the quiz's score policy. On a `highest` quiz a failing retake leaves this at the earlier pass, so do not infer it from `score_percentage`.",
                "example": 86
              }
            }
          },
          "review": {
            "type": "array",
            "nullable": true,
            "description": "Per-question outcome, or **null** when the attempt's edition had answer reveal off. Null is not an error and not an empty result — render `review_hidden_reason` instead of an empty list.",
            "items": {
              "$ref": "#/components/schemas/TrainingQuizAnswerReview"
            }
          },
          "review_hidden_reason": {
            "type": "string",
            "nullable": true,
            "enum": [
              "answers_not_revealed"
            ],
            "description": "Null when `review` is present."
          },
          "attempts": {
            "type": "object",
            "description": "The attempt pool, and every graded attempt for the \"your attempts\" switcher.",
            "properties": {
              "taken": {
                "type": "integer",
                "example": 2
              },
              "max": {
                "type": "integer",
                "nullable": true,
                "example": 3
              },
              "remaining": {
                "type": "integer",
                "nullable": true,
                "example": 1
              },
              "graded": {
                "type": "array",
                "description": "Newest first. Every graded attempt is listed, including ones the standing verdict did NOT come from — without this a learner could only reach them by guessing an id. `current` flags the one being shown.",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "attempt_number": {
                      "type": "integer"
                    },
                    "score_percentage": {
                      "type": "integer",
                      "nullable": true
                    },
                    "passed": {
                      "type": "boolean"
                    },
                    "timed_out": {
                      "type": "boolean"
                    },
                    "submitted_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "current": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "retake": {
            "type": "object",
            "description": "Whether to offer a Retake at all. Read from the same gates as the card, so the two surfaces cannot disagree.",
            "properties": {
              "allowed": {
                "type": "boolean"
              },
              "reason": {
                "type": "string",
                "nullable": true,
                "description": "Learner-facing sentence when not allowed; display verbatim."
              },
              "next_available_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              }
            }
          }
        }
      },
      "TrainingOfflineLesson": {
        "type": "object",
        "description": "One lesson packaged for storage on the device. Everything here is either the body itself or something the client needs to make that body work with no server.",
        "properties": {
          "content_type": {
            "type": "string",
            "enum": [
              "text",
              "video",
              "document",
              "quiz",
              "scorm",
              "partner_course"
            ],
            "example": "text"
          },
          "offline_supported": {
            "type": "boolean",
            "description": "Whether this lesson can be stored and rendered with no connection. COMPUTED HERE ON PURPOSE — a client could derive it from `content_type` plus the video provider, but then every platform re-implements the rule and they drift. Same argument as `can_register` on the sessions endpoints.",
            "example": true
          },
          "offline_blocked_by": {
            "type": "string",
            "nullable": true,
            "enum": [
              "online_only_runtime",
              "external_video"
            ],
            "description": "WHY it cannot go offline, so the client can say so rather than silently omitting the lesson. `online_only_runtime` — a `scorm` or `partner_course` lesson, whose runtime is not ours to ship, or a TIMED `quiz` (an untimed quiz IS offline-capable — see `quiz`). `external_video` — a YouTube/Vimeo embed. Null when `offline_supported` is true.",
            "example": null
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "The author's lesson description, sanitised like `html`, for EVERY lesson type — the line the online lesson page prints under its eyebrow. `html` folds it in only for non-text lessons, so a stored text lesson reads it from here. Null when the author left it blank.",
            "example": "Configure your physical locations"
          },
          "quiz": {
            "type": "object",
            "nullable": true,
            "description": "For an UNTIMED quiz lesson only: the quiz's sealed edition, answer-stripped exactly as the live player is (no correct answers, matching addressed by opaque tokens, hotspot regions withheld), so the assessment can be taken with no connection and replayed through `POST /training/quiz_attempts/offline`. Key that replay's `answers` by these question ids. Null for every other lesson and for a timed quiz.",
            "properties": {
              "id": {
                "type": "integer"
              },
              "version_id": {
                "type": "integer",
                "description": "The sealed edition the ids belong to."
              },
              "title": {
                "type": "string"
              },
              "instructions": {
                "type": "string",
                "nullable": true
              },
              "passing_score": {
                "type": "integer"
              },
              "question_display": {
                "type": "string",
                "enum": [
                  "one_per_page",
                  "all_at_once"
                ]
              },
              "question_count": {
                "type": "integer"
              },
              "max_attempts": {
                "type": "integer",
                "nullable": true
              },
              "questions": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TrainingQuizQuestion"
                }
              }
            }
          },
          "html": {
            "type": "string",
            "nullable": true,
            "description": "The lesson body, SANITISED through the same allowlist the web renders with (`training_rich_text`), so offline and online show byte-identical markup and the allowlist has one definition. Never the raw column: raw author markup can carry a `<script>`, an `onerror=` or a `javascript:` href, and an offline copy re-executes it every time the lesson opens — including when nothing can reach the device to revoke it.\n\nFor a TEXT lesson this is the authored body. For every other type it is the lesson's description, which the client renders above whatever native surface it builds for the media. Null when there is neither.",
            "example": "<p>Before servicing any powered equipment…</p>"
          },
          "stylesheets": {
            "type": "array",
            "description": "Absolute, digested URLs of the CSS this html needs — download each and rewrite the `<link>` hrefs. The SAME files the online webview links.\n\nTEXT LESSONS GET A THIRD ENTRY (`custom.css`) and the others do not: it is 438 KB and carries only the `.lesson-content` rules, which exist because the editor emits bullet AND numbered lists as `<ol>` — without them every bullet list an author typed renders as \"1. 2. 3.\". A client syncing video or document lessons should not pay for it.",
            "items": {
              "type": "string"
            },
            "example": [
              "https://officechat.workforce.mangoapps.com/assets/mobile_tailwind-8f3a.css",
              "https://officechat.workforce.mangoapps.com/vendor/fontawesome-pro-6.7.2-web/css/all.min.css"
            ]
          },
          "document": {
            "type": "object",
            "nullable": true,
            "description": "The lesson's attached document, for a `document` lesson. A FIELD, not markup — it is a lesson-level attachment and was never part of the body, so parsing the html could not find it. Render it with the platform's own viewer.\n\n`url` IS SIGNED AND EXPIRING — download it, then point the stored copy at the local file. Null when nothing is attached.",
            "properties": {
              "url": {
                "type": "string",
                "example": "https://officechat.workforce.mangoapps.com/rails/active_storage/blobs/redirect/xyz/handbook.pdf"
              },
              "filename": {
                "type": "string",
                "example": "handbook.pdf"
              },
              "content_type": {
                "type": "string",
                "example": "application/pdf"
              },
              "byte_size": {
                "type": "integer",
                "example": 284913
              }
            }
          },
          "video": {
            "type": "object",
            "nullable": true,
            "description": "The lesson's video, for a `video` lesson. Read `downloadable` FIRST — the two cases have different fields and only one can go offline.",
            "properties": {
              "downloadable": {
                "type": "boolean",
                "description": "True for an UPLOADED video (url/filename/byte_size present). False for an embed, where only `provider` and `embed_url` are.",
                "example": true
              },
              "url": {
                "type": "string",
                "nullable": true,
                "description": "Signed and expiring",
                "same as `document.url`.": null
              },
              "filename": {
                "type": "string",
                "nullable": true
              },
              "content_type": {
                "type": "string",
                "nullable": true
              },
              "byte_size": {
                "type": "integer",
                "nullable": true
              },
              "duration_seconds": {
                "type": "integer",
                "nullable": true,
                "example": 742
              },
              "provider": {
                "type": "string",
                "nullable": true,
                "description": "Present only when not downloadable.",
                "example": "youtube"
              },
              "embed_url": {
                "type": "string",
                "nullable": true,
                "description": "Present only when not downloadable — requires a connection."
              }
            }
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the lesson was last edited. Compare against what you stored to decide whether a re-sync needs to re-download this lesson at all.",
            "example": "2026-08-28T09:14:00Z"
          }
        }
      },
      "TrainingLessonProgressInput": {
        "type": "object",
        "description": "What a client reports about one lesson. EVERY FIELD IS OPTIONAL — send only what changed. An empty body is valid and just stamps the resume position.",
        "properties": {
          "completed": {
            "type": "boolean",
            "description": "`true` completes the lesson. There is no `false` — completion is monotonic, so sending false does not un-complete (see the endpoint description). Safe to send repeatedly.",
            "example": true
          },
          "video_position": {
            "type": "integer",
            "description": "Seconds into the video. Stored as the MAX of this and what is already recorded, so an out-of-order flush cannot rewind the learner.",
            "example": 120
          },
          "time_spent": {
            "type": "integer",
            "description": "Seconds spent in THIS sitting — a delta, added to the stored total, not a total itself. Omit it and the server records wall-clock time from first open to completion instead (what the web records); send it and your figure wins, which is what an offline client wants, since a flush can arrive days after the lesson was finished.",
            "example": 300
          }
        }
      },
      "TrainingLessonProgress": {
        "type": "object",
        "description": "The lesson's progress AFTER the write, plus the two enrollment numbers a client would otherwise refetch to update the course row it came from.",
        "properties": {
          "lesson_id": {
            "type": "integer",
            "example": 91
          },
          "completed": {
            "type": "boolean",
            "example": true
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": "2026-09-01T12:35:49Z"
          },
          "video_position": {
            "type": "integer",
            "nullable": true,
            "description": "The stored value",
            "which may be HIGHER than what you just sent (max wins).": null,
            "example": 120
          },
          "time_spent_seconds": {
            "type": "integer",
            "nullable": true,
            "example": 300
          },
          "score": {
            "type": "integer",
            "nullable": true,
            "description": "Read-only — written by SCORM/xAPI callbacks and partner-content sync. A progress write never sets it; a learner does not grade themselves.",
            "example": 88
          },
          "course_progress_percentage": {
            "type": "integer",
            "description": "The enrollment's recalculated percentage — so a client can update the course card without a second call.",
            "example": 62
          },
          "course_status": {
            "type": "string",
            "enum": [
              "enrolled",
              "in_progress",
              "completed",
              "cancelled"
            ],
            "example": "in_progress"
          }
        }
      },
      "TrainingSessionRegistrationSummary": {
        "type": "object",
        "nullable": true,
        "description": "The caller's hold for the course in WHATEVER state — including `attended` and `completed`, which the per-row `cta_action` deliberately ignores so a past attendance cannot make every other row offer \"switch\". Null when the caller has no live hold.\n\n**This block is self-contained on purpose.** It describes a session that may not be anywhere else in the response: the session list is `scheduled` + `upcoming`, so a seat the learner has already ATTENDED is in the past and appears in no list. It therefore carries the session's title, type and full time — never just `session_id`, which a client would have no row to resolve against.\n\n**Render `starts_at` in `timezone`.** The instant is given once, in UTC, plus the IANA zone of the VENUE — an instructor-led session happens in a specific place, so that zone is the one that matters, never the viewer's. Do NOT convert into the device's zone (a bare `new Date(starts_at)`) or you will show a learner in another country a time the session does not start at. `timezone_label` is the short form for display (`CDT`).\n\n`starts_at_local` / `ends_at_local` were emitted until 2026-09-01 and are gone — derivable from `starts_at` + `timezone` on every client platform.\n\nIdentical shape from every endpoint that reports a hold: the course detail, the session list, a single session, and the register/cancel responses.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "session_id": {
            "type": "integer"
          },
          "session_title": {
            "type": "string",
            "nullable": true,
            "description": "Title of the held session, so it renders without a list row."
          },
          "session_type": {
            "type": "string",
            "nullable": true,
            "enum": [
              "classroom",
              "webinar",
              "hybrid"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "registered",
              "waitlisted",
              "attended",
              "completed",
              "no_show",
              "cancelled"
            ]
          },
          "waitlisted": {
            "type": "boolean"
          },
          "waitlist_position": {
            "type": "integer",
            "nullable": true
          },
          "switchable": {
            "type": "boolean",
            "description": "Whether this hold can still be moved or cancelled (registered or waitlisted only)."
          },
          "awaiting_sign_off": {
            "type": "boolean",
            "description": "Attended, but still needs an instructor's sign-off."
          },
          "starts_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "UTC. For sorting and arithmetic, not for display."
          },
          "ends_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "timezone": {
            "type": "string",
            "nullable": true,
            "description": "IANA name of the session's zone, e.g. `America/Chicago`."
          },
          "timezone_label": {
            "type": "string",
            "nullable": true,
            "description": "Short form for humans, e.g. `CDT`."
          },
          "duration_minutes": {
            "type": "integer",
            "nullable": true
          }
        }
      },
      "TrainingScormCard": {
        "type": "object",
        "description": "The courseware launch panel — see `GET .../lessons/{lesson_id}/scorm`. The lesson type is labelled \"SCORM\" everywhere in the product, but the engine behind it plays SCORM 1.2, SCORM 2004, xAPI, cmi5 and AICC; `package.standard` is what says which one a given module is.",
        "properties": {
          "lesson_id": {
            "type": "integer",
            "nullable": true,
            "example": 289
          },
          "lesson_title": {
            "type": "string",
            "nullable": true,
            "example": "Spill Response Simulation"
          },
          "package": {
            "type": "object",
            "description": "The imported, playable form of the module. Null-ish throughout for a lesson whose package has never been imported — which is a real state, not a bug: see `launchable`.",
            "properties": {
              "standard": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "scorm12",
                  "scorm2004",
                  "xapi",
                  "cmi5",
                  "aicc"
                ]
              },
              "standard_label": {
                "type": "string",
                "nullable": true,
                "description": "How the standard is spelled for a learner — `SCORM`, `xAPI`, `cmi5`, `AICC`. Print this rather than deriving it from `standard`.",
                "example": "SCORM"
              },
              "title": {
                "type": "string",
                "nullable": true,
                "description": "The manifest's own title, which authors routinely write differently from the lesson title. Reported BESIDE `lesson_title`, not instead of it — the web cards show the lesson's."
              },
              "status": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "pending",
                  "processing",
                  "ready",
                  "failed"
                ],
                "description": "The LESSON's import state. Diagnostic only — it is not the gate, and a tenant carrying lessons from before the native engine has them stamped `ready` with no package behind them."
              },
              "launchable": {
                "type": "boolean",
                "description": "**The gate.** True when a launch can actually produce a player: the package imported and its files landed (or it is an AICC unit that runs on the provider's own server). This is the same check the launch itself applies, so render the primary control from here.",
                "example": true
              },
              "multi_unit": {
                "type": "boolean",
                "description": "A menu-driven package (several SCOs / AUs). The player owns the menu; the card reports the shape so it can say \"6 of 18 sections\"."
              },
              "unit_count": {
                "type": "integer",
                "nullable": true,
                "example": 1
              },
              "mastery_score": {
                "type": "string",
                "nullable": true,
                "description": "The bar the manifest sets, verbatim (a string in the manifest).",
                "example": "80"
              },
              "time_limit_seconds": {
                "type": "integer",
                "nullable": true,
                "description": "The manifest's max time allowed, parsed to seconds. The player enforces it; the card reports it so a learner knows before starting.",
                "example": 1200
              },
              "error": {
                "type": "string",
                "nullable": true,
                "description": "Why the import failed, as the importer recorded it.",
                "example": "No imsmanifest.xml in the ZIP"
              }
            }
          },
          "completion": {
            "type": "object",
            "nullable": true,
            "description": "The learner's record for this lesson — the Status and Score tiles the web cards show. Null until the module is first launched: a card view is a read and creates nothing, so an untouched module never reads as started.",
            "properties": {
              "completed": {
                "type": "boolean"
              },
              "completed_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "started_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "score": {
                "type": "integer",
                "nullable": true,
                "description": "0–100, projected from the runtime's own score. Null for a package that reports none, which many do — do not render 0 for it.\n\n**FROZEN once `completed` is true.** The commit that completes the lesson records its score and training time; nothing the runtime reports afterwards can change either. So this figure is final — safe to render as \"your score\" rather than \"your latest score\" — and a replay can neither improve nor damage it.",
                "example": 92
              },
              "time_spent_seconds": {
                "type": "integer",
                "nullable": true,
                "description": "Frozen at completion, like `score`. Before that it is the greater of the runtime's reported time and the wall clock since the lesson was started, so it never goes backwards.",
                "example": 900
              }
            }
          },
          "resume": {
            "type": "object",
            "nullable": true,
            "description": "Where the learner is INSIDE the module. Null when there is nothing to resume (never launched, or reset), so a fresh card stays fresh.",
            "properties": {
              "sentence": {
                "type": "string",
                "nullable": true,
                "description": "The one line the web cards print, composed server-side because the per-standard phrasing rule is the part that drifts — e.g. \"Resume from Etiquette · 6 of 18 sections done · 42 min in\".",
                "example": "Resume where you left off · 7m 00s in"
              },
              "percent": {
                "type": "integer",
                "nullable": true,
                "description": "0–100, for the card's progress bar. Null when the package reports no measure.",
                "example": 37
              },
              "units_total": {
                "type": "integer",
                "example": 18
              },
              "units_done": {
                "type": "integer",
                "example": 6
              },
              "current_unit_title": {
                "type": "string",
                "nullable": true,
                "example": "Etiquette"
              },
              "bookmarked": {
                "type": "boolean",
                "description": "The module reported a location or suspend data, so a relaunch resumes."
              },
              "finished": {
                "type": "boolean"
              },
              "time_spent": {
                "type": "string",
                "nullable": true,
                "description": "Humanised total, e.g. `42m 10s` / `1h 05m`. Null under a minute, where it reads as noise rather than progress."
              },
              "launches": {
                "type": "integer",
                "description": "How many times the module has been opened. **Not attempts** — courseware has no attempt pool; see the endpoint description.",
                "example": 2
              },
              "status": {
                "type": "string",
                "enum": [
                  "not_attempted",
                  "incomplete",
                  "completed",
                  "passed",
                  "failed",
                  "browsed"
                ],
                "description": "The runtime's own status for the registration."
              }
            }
          },
          "offline": {
            "type": "object",
            "description": "Always unsupported for courseware — the runtime is server-side, so there is nothing a client can store and play offline. Reported on the card because the launch control is where a learner decides whether to start something with no signal.",
            "properties": {
              "supported": {
                "type": "boolean",
                "example": false
              },
              "blocked_by": {
                "type": "string",
                "nullable": true,
                "example": "online_only_runtime"
              }
            }
          },
          "cta": {
            "type": "object",
            "description": "The one primary control, resolved server-side so three clients cannot each invent their own ladder.",
            "properties": {
              "action": {
                "type": "string",
                "enum": [
                  "launch",
                  "resume",
                  "review",
                  "processing",
                  "unavailable"
                ]
              },
              "label": {
                "type": "string",
                "description": "The verb the web cards print for this action.",
                "example": "Launch module"
              },
              "web_view_url": {
                "type": "string",
                "nullable": true,
                "description": "The chrome-less runtime to open in a WebView, absolute. **Null whenever there is nothing to open** — render the control only when it is present. Opening it STARTS the attempt (it mints the registration and a single-use session), so never prefetch it.",
                "example": "https://acme.workforce.mangoapps.com/m/apps/training/courses/81/lessons/289/scorm?embed=1"
              },
              "reason": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "package_processing",
                  "package_stalled",
                  "package_unavailable",
                  "enrollment_cancelled"
                ],
                "description": "Why there is nothing to launch. `package_stalled` is an import that stopped part-way and will not resume on its own — it needs an admin to re-upload, which `message` says."
              },
              "message": {
                "type": "string",
                "nullable": true,
                "description": "Prose written for a learner. Show it verbatim rather than mapping the code.",
                "example": "This module is still being prepared. Check back shortly."
              },
              "secondary": {
                "type": "object",
                "nullable": true,
                "description": "\"Start over\" (`POST .../scorm/restart`) — discards the bookmark so the module plays from the beginning. Present ONLY on an unfinished module that has something to discard; null on a `review` card, because a completed lesson's record is frozen and a reset there would promise a do-over that cannot be recorded.",
                "properties": {
                  "action": {
                    "type": "string",
                    "enum": [
                      "restart"
                    ],
                    "example": "restart"
                  },
                  "label": {
                    "type": "string",
                    "example": "Start over"
                  }
                }
              }
            }
          }
        }
      },
      "WorkspacePaginationMeta": {
        "type": "object",
        "description": "Pagination state for the page just returned. The same values are also sent as the `X-Total-Count`, `X-Total-Pages`, `X-Current-Page` and `X-Per-Page` response headers.",
        "properties": {
          "total_count": {
            "type": "integer",
            "description": "Rows matching the request, across all pages."
          },
          "total_pages": {
            "type": "integer"
          },
          "current_page": {
            "type": "integer"
          },
          "per_page": {
            "type": "integer",
            "description": "The page size actually used, after clamping."
          },
          "has_next_page": {
            "type": "boolean"
          },
          "has_prev_page": {
            "type": "boolean"
          }
        }
      },
      "WorkspaceUserRef": {
        "type": "object",
        "description": "A compact reference to a platform user.",
        "properties": {
          "id": {
            "type": "integer",
            "nullable": true
          },
          "email": {
            "type": "string",
            "nullable": true
          },
          "name": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "Workspace": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "slug": {
            "type": "string",
            "description": "URL-safe identifier; accepted anywhere an id is."
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string",
            "description": "The workspace's lifecycle state, e.g. `active` or `archived`."
          },
          "icon": {
            "type": "string",
            "nullable": true
          },
          "color": {
            "type": "string",
            "nullable": true
          },
          "owner_id": {
            "type": "integer",
            "description": "The user who owns the workspace."
          },
          "role": {
            "type": "string",
            "nullable": true,
            "description": "The CALLER's role in this workspace — e.g. `owner`, `member`, `viewer`. A rule-group member with no explicit membership row reports `member`. Null when the caller has no role in it."
          },
          "member_count": {
            "type": "integer",
            "description": "Explicit memberships on the workspace."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WorkspaceTemplate": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "icon": {
            "type": "string",
            "nullable": true
          },
          "color": {
            "type": "string",
            "nullable": true
          },
          "system_template": {
            "type": "boolean",
            "description": "True for the platform-provided set, false for the tenant's own."
          },
          "structure": {
            "type": "object",
            "description": "The blueprint a workspace created from this template receives — sections, task lists and seed content. `{}` when the template defines none."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WorkspaceMessage": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "workspace_id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "body": {
            "type": "string"
          },
          "external_id": {
            "type": "string",
            "nullable": true,
            "description": "Caller-owned durable publication identity when one was supplied."
          },
          "internal_only": {
            "type": "boolean",
            "description": "Excluded from the client digest and the client portal when true."
          },
          "pinned": {
            "type": "boolean"
          },
          "author": {
            "$ref": "#/components/schemas/WorkspaceUserRef"
          },
          "comment_count": {
            "type": "integer",
            "description": "Comments on the thread. On the detail endpoint this is the TRUE total, not the size of the `comments` page returned beside it."
          },
          "web_url": {
            "type": "string",
            "format": "uri",
            "description": "Browser URL for the message in the tenant."
          },
          "attachments": {
            "type": "array",
            "description": "Present on detail and mutation responses; omitted from the message-board list.",
            "items": {
              "$ref": "#/components/schemas/WorkspaceMessageAttachment"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WorkspaceMessageAttachment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "filename": {
            "type": "string"
          },
          "content_type": {
            "type": "string"
          },
          "byte_size": {
            "type": "integer"
          },
          "checksum": {
            "type": "string",
            "description": "Base64-encoded Active Storage checksum for durable client-side deduplication."
          },
          "inline_playable": {
            "type": "boolean"
          },
          "inline_url": {
            "type": "string",
            "format": "uri"
          },
          "download_url": {
            "type": "string",
            "format": "uri"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WorkspaceMessageComment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "body": {
            "type": "string"
          },
          "internal_only": {
            "type": "boolean"
          },
          "author": {
            "type": "object",
            "description": "The commenter. A reply posted through the Workspace Client Portal has no platform account: `id` is null, `email` is the client's address and `external` is true.",
            "properties": {
              "id": {
                "type": "integer",
                "nullable": true
              },
              "email": {
                "type": "string",
                "nullable": true
              },
              "name": {
                "type": "string",
                "nullable": true
              },
              "external": {
                "type": "boolean"
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WorkspaceTask": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "workspace_id": {
            "type": "integer"
          },
          "list_id": {
            "type": "integer",
            "nullable": true,
            "description": "The task list this task sits in."
          },
          "title": {
            "type": "string"
          },
          "notes": {
            "type": "string",
            "nullable": true
          },
          "due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "completed": {
            "type": "boolean"
          },
          "assignee": {
            "allOf": [
              {
                "$ref": "#/components/schemas/WorkspaceUserRef"
              }
            ],
            "nullable": true,
            "description": "Null when the task is unassigned."
          },
          "ai_drafted": {
            "type": "boolean",
            "description": "True when the task was proposed by the Workspace agent."
          },
          "internal_only": {
            "type": "boolean"
          },
          "recurring_cadence": {
            "type": "string",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WorkspaceCheckIn": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "workspace_id": {
            "type": "integer"
          },
          "question": {
            "type": "string"
          },
          "cadence": {
            "type": "string",
            "description": "How often the question is asked, e.g. `weekly`."
          },
          "active": {
            "type": "boolean"
          },
          "next_run_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WorkspaceCheckInDraft": {
        "type": "object",
        "description": "An AI-drafted check-in response of the caller's, awaiting approval.",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Pass this as `response_id` to the approve endpoint."
          },
          "question": {
            "type": "string",
            "nullable": true,
            "description": "The check-in question this drafts an answer to."
          },
          "response_text": {
            "type": "string",
            "nullable": true
          },
          "drafted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "ai_drafted": {
            "type": "boolean"
          }
        }
      },
      "WorkspaceEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "workspace_id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "location": {
            "type": "string",
            "nullable": true,
            "description": "Free text; this is where meeting links usually live."
          },
          "all_day": {
            "type": "boolean"
          },
          "starts_at": {
            "type": "string",
            "format": "date-time"
          },
          "ends_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "internal_only": {
            "type": "boolean",
            "description": "Excluded from the client digest and the client portal when true. Carried so a client-facing calendar can filter before publishing."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WorkspaceHillChart": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "workspace_id": {
            "type": "integer"
          },
          "scope_label": {
            "type": "string",
            "description": "The named piece of work this chart tracks."
          },
          "position": {
            "type": "integer",
            "description": "Position on the hill, 0-100. The uphill half is \"figuring it out\", the downhill half \"making it happen\"."
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "archived"
            ]
          },
          "ai_proposed_position": {
            "type": "integer",
            "nullable": true,
            "description": "The agent's suggested position, when one is pending."
          },
          "ai_proposal_pending": {
            "type": "boolean",
            "description": "True when a suggestion is awaiting a human decision."
          },
          "updated_by_agent_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WorkspaceReaction": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "emoji": {
            "type": "string"
          },
          "reactable_type": {
            "type": "string",
            "enum": [
              "WorkspaceMessage",
              "WorkspaceMessageComment",
              "Task"
            ]
          },
          "reactable_id": {
            "type": "integer"
          },
          "user": {
            "$ref": "#/components/schemas/WorkspaceUserRef"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "Workspace",
      "description": "Workspace app — project workspaces and their canvas sections (message board, tasks, recurring check-ins, schedule, hill charts, reactions), plus the workspace templates and the client digest. API tokens require read:workspace for GET/HEAD and write:workspace for mutations; app, tenant, membership, role, and feature checks still apply."
    },
    {
      "name": "Frontline Execution",
      "description": "Frontline Execution worker surface — shift pass-down (handover between shifts)"
    },
    {
      "name": "Live Boards",
      "description": "Live Boards — boards with their current readings, and the rotations built from them (read-only; the app is metered on active board objects, so no write endpoints are offered)"
    },
    {
      "name": "Surveys",
      "description": "Surveys app — the surveys assigned to the caller, a survey with its questions, submitting a response, the anonymous feedback channel, and anonymity-safe aggregate results plus reminders for the people who run a survey. Per-survey authorization is Surveys::AccessPolicy, so the results payload differs by tier; raw per-respondent answers are never returned."
    },
    {
      "name": "Ideas",
      "description": "Ideas app dashboard (totals, stage counts, top-voted / recent / most-discussed)"
    },
    {
      "name": "Comms Hub",
      "description": "Communications (Comms Hub / News Feed) home dashboard — unread/ack/verified counts, open poll, latest posts, must-read status, trending topics (read-only)"
    },
    {
      "name": "Recognitions",
      "description": "Recognitions app dashboard — received/given/nomination stats, the activity stream, my awards, my nominations, trending recognition, active programs, the running award cycle and this month's top recipients (read-only)"
    },
    {
      "name": "Company Store",
      "description": "Company Store app — the employee rewards storefront. Dashboard: the wallet, the four stat tiles, the \"within reach\" affordability carousel, featured and saved items, the 30-day points-activity roll-up, recent orders, the first-run earn on-ramp and the manager-only team-approvals widget. Catalog: the paginated grid with the web page's own category / collection / featured / search filters, its six sort strategies and region scoping, the filter sheet with counts, and one item's detail (photo strip, variant groups, per-method payment availability, Item Details, related strip). Orders: order history with per-status counts and one order's full detail (derived lifecycle timeline, reward payload, shipping, available actions). Approvals: the redemption approval queue plus its approve / decline decisions — the one write surface here, and the one endpoint group open ONLY to redemption approvers (a designated approver-group member, or a line manager with direct reports when no group is configured). Everything else is read-only"
    },
    {
      "name": "Wikis",
      "description": "Wikis knowledge-base dashboard, browse tree, and search (read-only)"
    },
    {
      "name": "Libraries",
      "description": "Libraries app — curated link / file / media collections. The All Libraries index mirrors the web page card-for-card (cover, mark, kind, category and item counts, last-updated, capability flags) with the all / disabled filters, the three orders (Admin order · A → Z · Recently updated) and pagination; disabled libraries always sort last. Visibility is audience-scoped — a member never sees a disabled library or one whose audience rules exclude them. Libraries administrators can additionally DISABLE a library (POST /libraries/{id}/disable) — a reversible hide that mirrors the web control, is idempotent rather than a toggle, and requires the `write:libraries` scope. The library detail read (GET /libraries/{id}) returns the whole library screen in one call — the header, every category with the layout and sort order its admin set on it, and every item with its format, provenance, resolved destination, download and pre-gated kebab actions. It is reachable only by a caller who can access that library: a restricted, disabled-and-unmanageable, cross-tenant or non-existent id all return the same 404."
    },
    {
      "name": "Training",
      "description": "Training app learner \"My Training\" home — stats, continue learning, upcoming instructor-led sessions, and the status-filtered course-enrollment list (read-only)"
    },
    {
      "name": "Leader Rounds",
      "description": "Leader Rounds — structured leader rounding on staff. The due list, the capture form's typed question set, round detail across both visibility tiers, the derived stoplight issue ledger, coverage rolled up the reporting line, and the subject-tier close-the-loop view (rounds about you, what you raised, what you own)."
    },
    {
      "name": "Authentication",
      "description": "Mobile app authentication endpoints"
    },
    {
      "name": "Two-Factor Authentication",
      "description": "Two-factor authentication self-service management"
    },
    {
      "name": "Passwordless Authentication",
      "description": "Passwordless authentication via magic links and codes"
    },
    {
      "name": "API Token Management",
      "description": "Personal API token creation and management"
    },
    {
      "name": "Impersonation",
      "description": "Impersonation token management for service accounts to act on behalf of users"
    },
    {
      "name": "Service Desk",
      "description": "Support ticket management and help desk operations"
    },
    {
      "name": "RFP Desk",
      "description": "RFP Desk — answer library, RFP responses and their sections, and bid/no-bid assessments"
    },
    {
      "name": "TinyTake",
      "description": "Screen capture, video recording, and file management for the TinyTake marketplace app"
    },
    {
      "name": "Employee Compensation",
      "description": "Employee compensation profile, history, and insights management"
    },
    {
      "name": "Compensation Requests",
      "description": "Compensation change request lifecycle management"
    },
    {
      "name": "Users",
      "description": "User profile and preferences management"
    },
    {
      "name": "User Profile Management",
      "description": "Enhanced user profile management with actions and bulk operations"
    },
    {
      "name": "Account Settings",
      "description": "Comprehensive account settings management including security, privacy, and communication preferences"
    },
    {
      "name": "Leave Management",
      "description": "Employee leave requests, balances, and leave type management"
    },
    {
      "name": "Forms",
      "description": "Form templates, submissions, and mobile rendering with offline support"
    },
    {
      "name": "Marketplace Apps",
      "description": "Enabled marketplace apps for mobile app launcher"
    },
    {
      "name": "System Modules",
      "description": "Core platform features that can be enabled/disabled per business"
    },
    {
      "name": "Skills & Certifications",
      "description": "Employee skills management, certifications, and competency tracking"
    },
    {
      "name": "ActionCue Webhooks",
      "description": "ActionCue external processing system webhook endpoints"
    },
    {
      "name": "Shifts",
      "description": "Shift management and scheduling"
    },
    {
      "name": "Attendance",
      "description": "Time tracking and attendance management"
    },
    {
      "name": "Timesheets",
      "description": "Employee timesheet management and submission"
    },
    {
      "name": "Timesheet Entries",
      "description": "Individual timesheet entry management and editing"
    },
    {
      "name": "Pay Information",
      "description": "Employee pay summaries, history, and year-to-date information"
    },
    {
      "name": "Paychecks",
      "description": "Employee paycheck access and detailed pay stub information"
    },
    {
      "name": "Pay Periods",
      "description": "Pay period management and detailed period information"
    },
    {
      "name": "Manual Time Entries",
      "description": "Manual time entry creation and management for missed punches"
    },
    {
      "name": "EPMS Dashboard",
      "description": "Employee Performance Management System dashboard with aggregated goals, reviews, feedback, and meetings"
    },
    {
      "name": "EPMS Goals",
      "description": "Employee goal management including creation, tracking, progress updates, and completion"
    },
    {
      "name": "EPMS Performance Reviews",
      "description": "Performance review lifecycle management including creation, self-assessments, approvals, and goal linking"
    },
    {
      "name": "EPMS Continuous Feedback",
      "description": "Continuous feedback system for real-time performance feedback including praise, recognition, and coaching"
    },
    {
      "name": "EPMS Development Plans",
      "description": "Employee development plan management with goal tracking and skill development"
    },
    {
      "name": "EPMS Meetings",
      "description": "One-on-one meetings and check-ins scheduling and management"
    },
    {
      "name": "EPMS Competency Frameworks",
      "description": "Competency framework definitions and structure (read-only for most users)"
    },
    {
      "name": "EPMS Competency Assessments",
      "description": "Competency assessment creation and management with ratings and evaluations"
    },
    {
      "name": "Notifications",
      "description": "User notification management — mark read/unread, archive, bulk operations (mobile support)"
    },
    {
      "name": "Messaging",
      "description": "Inbox → Messages (Direct Messages) — 1:1 / small-group conversations, message compose (text + attachments), edit, delete, participants, and recipient picker"
    },
    {
      "name": "Chat",
      "description": "Chat app — direct / group / channel rooms, async message posting with media, reactions, threads, important-message acknowledgements, mentions, search, presence, and Pusher real-time bootstrap"
    },
    {
      "name": "Safety Hub",
      "description": "Incidents, safety observations, toolbox talks, certifications, and the personal submission feed"
    },
    {
      "name": "Inspections",
      "description": "Inspections — role-aware list endpoint (personal by default, team feed via `?team=true` for managers)"
    },
    {
      "name": "News Feed",
      "description": "News Feed posts (Update / Question / Poll), pinning, comments, reactions, read receipts, and must-read acknowledgements"
    },
    {
      "name": "Broadcasts",
      "description": "Company broadcasts — the caller's received inbox (Dashboard parity) with all/unread/critical/acknowledge subfilters, plus create/publish/acknowledge"
    },
    {
      "name": "Alerts",
      "description": "Emergency alerts — the caller's received inbox + pending approvals, with all/urgent/acknowledge/safety_check_in subfilters and per-caller response state"
    },
    {
      "name": "Approvals",
      "description": "Shared approve/reject for Comms Hub approval requests — one endpoint pair for both broadcasts and alerts (act on the request id from a source's pending_approvals)"
    },
    {
      "name": "Audience Options",
      "description": "\"Add to audience\" picker options shared by the broadcast + alert composers — departments / locations / job titles / roles / groups, each with a user_count (search + pagination)"
    },
    {
      "name": "AI Notepad",
      "description": "AI Notepad — meetings and notes with their transcripts, AI artifacts, action items, notebooks, sharing and streamed AI chat. A row is both a recorded meeting and a plain typed note; `source` says which. Bearer token; this namespace declares no per-endpoint token scopes — the gate is that the AI Notepad app is enabled for the business and visible to the caller — except send_to_chat, which also needs write:chat."
    }
  ]
}